Skip to content

Commit 776ab9e

Browse files
authored
Allow executing scripts in workspace root (#1473)
1 parent 141f4f8 commit 776ab9e

File tree

5 files changed

+393
-56
lines changed

5 files changed

+393
-56
lines changed

scarb/src/bin/scarb/args.rs

+4
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ pub struct ScriptsRunnerArgs {
263263
#[command(flatten)]
264264
pub packages_filter: PackagesFilter,
265265

266+
/// Run the script in workspace root only.
267+
#[arg(long, default_value_t = false)]
268+
pub workspace_root: bool,
269+
266270
/// Arguments to pass to executed script.
267271
#[clap(allow_hyphen_values = true)]
268272
pub args: Vec<OsString>,

scarb/src/bin/scarb/commands/run.rs

+156-47
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,75 @@
1-
use std::collections::BTreeMap;
2-
use std::ffi::OsString;
3-
use std::fmt::Write;
4-
51
use anyhow::{anyhow, Result};
62
use indoc::formatdoc;
7-
use serde::{Serialize, Serializer};
8-
use smol_str::SmolStr;
9-
3+
use itertools::Itertools;
104
use scarb::core::errors::ScriptExecutionError;
11-
use scarb::core::{Config, Package, Workspace};
5+
use scarb::core::{Config, Package, PackageName, ScriptDefinition, Workspace};
126
use scarb::ops;
137
use scarb_ui::Message;
8+
use serde::{Serialize, Serializer};
9+
use smol_str::SmolStr;
10+
use std::collections::BTreeMap;
11+
use std::ffi::OsString;
12+
use std::fmt::Write;
1413

1514
use crate::args::ScriptsRunnerArgs;
1615
use crate::errors::ErrorWithExitCode;
1716

1817
#[tracing::instrument(skip_all, level = "info")]
1918
pub fn run(args: ScriptsRunnerArgs, config: &Config) -> Result<()> {
2019
let ws = ops::read_workspace(config.manifest_path(), config)?;
21-
let packages = args.packages_filter.match_many(&ws)?;
22-
let errors = packages
20+
let errors = if args.workspace_root {
21+
run_for_workspace_root(args, &ws)
22+
.err()
23+
.into_iter()
24+
.collect_vec()
25+
} else {
26+
run_for_packages(args, &ws)?
27+
.into_iter()
28+
.filter_map(|res| res.err())
29+
.map(|res| anyhow!(res))
30+
.collect::<Vec<anyhow::Error>>()
31+
};
32+
build_exit_error(errors)
33+
}
34+
35+
fn run_for_workspace_root(args: ScriptsRunnerArgs, ws: &Workspace) -> Result<()> {
36+
args.script
37+
.map(|script| {
38+
let script_definition = ws.script(&script).ok_or_else(|| {
39+
missing_script_error(&script, "workspace root", " --workspace-root")
40+
})?;
41+
ops::execute_script(script_definition, &args.args, ws, ws.root(), None)
42+
})
43+
.unwrap_or_else(|| {
44+
ws.config()
45+
.ui()
46+
.print(ScriptsList::for_workspace_root(ws.scripts().clone()));
47+
Ok(())
48+
})
49+
}
50+
51+
fn run_for_packages(args: ScriptsRunnerArgs, ws: &Workspace) -> Result<Vec<Result<()>>> {
52+
Ok(args
53+
.packages_filter
54+
.match_many(ws)?
2355
.into_iter()
2456
.map(|package| {
2557
args.script
2658
.clone()
27-
.map(|script| run_script(script, &args.args, package.clone(), &ws))
28-
.unwrap_or_else(|| list_scripts(package, &ws))
59+
.map(|script| run_package_script(script, &args.args, package.clone(), ws))
60+
.unwrap_or_else(|| {
61+
ws.config().ui().print(ScriptsList::for_package(
62+
package.id.name.clone(),
63+
package.manifest.scripts.clone(),
64+
ws.is_single_package(),
65+
));
66+
Ok(())
67+
})
2968
})
30-
.filter_map(|res| res.err())
31-
.map(|res| anyhow!(res))
32-
.collect::<Vec<anyhow::Error>>();
69+
.collect_vec())
70+
}
71+
72+
fn build_exit_error(errors: Vec<anyhow::Error>) -> Result<()> {
3373
if errors.is_empty() {
3474
Ok(())
3575
} else {
@@ -50,63 +90,132 @@ pub fn run(args: ScriptsRunnerArgs, config: &Config) -> Result<()> {
5090
}
5191
}
5292

53-
fn run_script(script: SmolStr, args: &[OsString], package: Package, ws: &Workspace) -> Result<()> {
93+
fn run_package_script(
94+
script: SmolStr,
95+
args: &[OsString],
96+
package: Package,
97+
ws: &Workspace,
98+
) -> Result<()> {
5499
let script_definition = package.manifest.scripts.get(&script).ok_or_else(|| {
55100
let package_name = package.id.name.to_string();
56-
let package_selector = if !ws.is_single_package() {
57-
format!(" -p {package_name}")
58-
} else {
101+
let package_selector = if ws.is_single_package() {
59102
String::new()
103+
} else {
104+
format!(" -p {package_name}")
60105
};
61-
anyhow!(formatdoc! {r#"
62-
missing script `{script}` for package: {package_name}
63-
64-
To see a list of scripts, run:
65-
scarb run{package_selector}
66-
"#})
106+
missing_script_error(
107+
&script,
108+
&format!("package: {package_name}"),
109+
&package_selector,
110+
)
67111
})?;
68112
ops::execute_script(script_definition, args, ws, package.root(), None)
69113
}
70114

71-
fn list_scripts(package: Package, ws: &Workspace) -> Result<()> {
72-
let scripts = package
73-
.manifest
74-
.scripts
75-
.iter()
76-
.map(|(name, definition)| (name.to_string(), definition.to_string()))
77-
.collect();
78-
let package = package.id.name.to_string();
79-
let single_package = ws.is_single_package();
80-
ws.config().ui().print(ScriptsList {
81-
scripts,
82-
package,
83-
single_package,
84-
});
85-
Ok(())
115+
fn missing_script_error(script: &str, source: &str, selector: &str) -> anyhow::Error {
116+
anyhow!(formatdoc! {r#"
117+
missing script `{script}` for {source}
118+
119+
To see a list of scripts, run:
120+
scarb run{selector}
121+
"#})
86122
}
87123

88124
#[derive(Serialize, Debug)]
89-
struct ScriptsList {
125+
struct PackageScriptsList {
90126
package: String,
91127
scripts: BTreeMap<String, String>,
92128
single_package: bool,
93129
}
94130

95-
impl Message for ScriptsList {
96-
fn text(self) -> String {
131+
#[derive(Serialize, Debug)]
132+
struct WorkspaceScriptsList {
133+
scripts: BTreeMap<String, String>,
134+
}
135+
136+
#[derive(Serialize, Debug)]
137+
#[serde(untagged)]
138+
enum ScriptsList {
139+
ForPackage(PackageScriptsList),
140+
ForWorkspaceRoot(WorkspaceScriptsList),
141+
}
142+
143+
impl ScriptsList {
144+
pub fn for_package(
145+
package: PackageName,
146+
scripts: BTreeMap<SmolStr, ScriptDefinition>,
147+
single_package: bool,
148+
) -> Self {
149+
let scripts = scripts
150+
.iter()
151+
.map(|(name, definition)| (name.to_string(), definition.to_string()))
152+
.collect();
153+
Self::ForPackage(PackageScriptsList {
154+
package: package.to_string(),
155+
scripts,
156+
single_package,
157+
})
158+
}
159+
160+
pub fn for_workspace_root(scripts: BTreeMap<SmolStr, ScriptDefinition>) -> Self {
161+
let scripts = scripts
162+
.iter()
163+
.map(|(name, definition)| (name.to_string(), definition.to_string()))
164+
.collect();
165+
Self::ForWorkspaceRoot(WorkspaceScriptsList { scripts })
166+
}
167+
168+
fn scripts(&self) -> &BTreeMap<String, String> {
169+
match self {
170+
Self::ForPackage(p) => &p.scripts,
171+
Self::ForWorkspaceRoot(w) => &w.scripts,
172+
}
173+
}
174+
}
175+
176+
impl PackageScriptsList {
177+
pub fn text(&self) -> String {
97178
let mut text = String::new();
98179
write!(text, "Scripts available via `scarb run`",).unwrap();
99180
if !self.single_package {
100181
write!(text, " for package `{}`", self.package).unwrap();
101182
}
102183
writeln!(text, ":",).unwrap();
103-
for (name, definition) in self.scripts {
104-
writeln!(text, "{:<22}: {}", name, definition).unwrap();
105-
}
184+
write!(text, "{}", write_scripts(&self.scripts)).unwrap();
185+
text
186+
}
187+
}
188+
189+
impl WorkspaceScriptsList {
190+
pub fn text(&self) -> String {
191+
let mut text = String::new();
192+
writeln!(
193+
text,
194+
"Scripts available via `scarb run` for workspace root:",
195+
)
196+
.unwrap();
197+
write!(text, "{}", write_scripts(&self.scripts)).unwrap();
106198
text
107199
}
200+
}
201+
202+
fn write_scripts(scripts: &BTreeMap<String, String>) -> String {
203+
let mut text = String::new();
204+
for (name, definition) in scripts {
205+
writeln!(text, "{:<22}: {}", name, definition).unwrap();
206+
}
207+
text
208+
}
209+
210+
impl Message for ScriptsList {
211+
fn text(self) -> String {
212+
match self {
213+
Self::ForPackage(p) => p.text(),
214+
Self::ForWorkspaceRoot(w) => w.text(),
215+
}
216+
}
108217

109218
fn structured<S: Serializer>(self, ser: S) -> Result<S::Ok, S::Error> {
110-
self.scripts.serialize(ser)
219+
self.scripts().serialize(ser)
111220
}
112221
}

scarb/src/core/workspace.rs

+14-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ use anyhow::{anyhow, bail, Result};
55
use camino::{Utf8Path, Utf8PathBuf};
66
use itertools::Itertools;
77
use scarb_ui::args::PackagesSource;
8+
use smol_str::SmolStr;
89

910
use crate::compiler::Profile;
1011
use crate::core::config::Config;
1112
use crate::core::package::Package;
12-
use crate::core::{PackageId, Target};
13+
use crate::core::{PackageId, ScriptDefinition, Target};
1314
use crate::flock::Filesystem;
1415
use crate::{DEFAULT_TARGET_DIR_NAME, LOCK_FILE_NAME, MANIFEST_FILE_NAME};
1516

@@ -22,6 +23,7 @@ pub struct Workspace<'c> {
2223
members: BTreeMap<PackageId, Package>,
2324
manifest_path: Utf8PathBuf,
2425
profiles: Vec<Profile>,
26+
scripts: BTreeMap<SmolStr, ScriptDefinition>,
2527
root_package: Option<PackageId>,
2628
target_dir: Filesystem,
2729
}
@@ -33,6 +35,7 @@ impl<'c> Workspace<'c> {
3335
root_package: Option<PackageId>,
3436
config: &'c Config,
3537
profiles: Vec<Profile>,
38+
scripts: BTreeMap<SmolStr, ScriptDefinition>,
3639
) -> Result<Self> {
3740
let targets = packages
3841
.iter()
@@ -58,6 +61,7 @@ impl<'c> Workspace<'c> {
5861
root_package,
5962
target_dir,
6063
members: packages,
64+
scripts,
6165
})
6266
}
6367

@@ -75,6 +79,7 @@ impl<'c> Workspace<'c> {
7579
root_package,
7680
config,
7781
profiles,
82+
BTreeMap::new(),
7883
)
7984
}
8085

@@ -174,6 +179,14 @@ impl<'c> Workspace<'c> {
174179
names.dedup();
175180
names
176181
}
182+
183+
pub fn scripts(&self) -> &BTreeMap<SmolStr, ScriptDefinition> {
184+
&self.scripts
185+
}
186+
187+
pub fn script(&self, name: &SmolStr) -> Option<&ScriptDefinition> {
188+
self.scripts.get(name)
189+
}
177190
}
178191

179192
fn check_unique_targets(targets: &Vec<&Target>) -> Result<()> {

scarb/src/ops/workspace.rs

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ fn read_workspace_root<'c>(
101101
.parent()
102102
.expect("Manifest path must have parent.");
103103

104+
let scripts = workspace.scripts.unwrap_or_default();
104105
// Read workspace members.
105106
let mut packages = workspace
106107
.members
@@ -137,6 +138,7 @@ fn read_workspace_root<'c>(
137138
root_package,
138139
config,
139140
profiles,
141+
scripts,
140142
)
141143
} else {
142144
// Read single package workspace

0 commit comments

Comments
 (0)