Skip to content

Commit

Permalink
Allow executing scripts in workspace root (software-mansion#1473)
Browse files Browse the repository at this point in the history
  • Loading branch information
maciektr authored Jul 26, 2024
1 parent 141f4f8 commit 776ab9e
Show file tree
Hide file tree
Showing 5 changed files with 393 additions and 56 deletions.
4 changes: 4 additions & 0 deletions scarb/src/bin/scarb/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ pub struct ScriptsRunnerArgs {
#[command(flatten)]
pub packages_filter: PackagesFilter,

/// Run the script in workspace root only.
#[arg(long, default_value_t = false)]
pub workspace_root: bool,

/// Arguments to pass to executed script.
#[clap(allow_hyphen_values = true)]
pub args: Vec<OsString>,
Expand Down
203 changes: 156 additions & 47 deletions scarb/src/bin/scarb/commands/run.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,75 @@
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fmt::Write;

use anyhow::{anyhow, Result};
use indoc::formatdoc;
use serde::{Serialize, Serializer};
use smol_str::SmolStr;

use itertools::Itertools;
use scarb::core::errors::ScriptExecutionError;
use scarb::core::{Config, Package, Workspace};
use scarb::core::{Config, Package, PackageName, ScriptDefinition, Workspace};
use scarb::ops;
use scarb_ui::Message;
use serde::{Serialize, Serializer};
use smol_str::SmolStr;
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fmt::Write;

use crate::args::ScriptsRunnerArgs;
use crate::errors::ErrorWithExitCode;

#[tracing::instrument(skip_all, level = "info")]
pub fn run(args: ScriptsRunnerArgs, config: &Config) -> Result<()> {
let ws = ops::read_workspace(config.manifest_path(), config)?;
let packages = args.packages_filter.match_many(&ws)?;
let errors = packages
let errors = if args.workspace_root {
run_for_workspace_root(args, &ws)
.err()
.into_iter()
.collect_vec()
} else {
run_for_packages(args, &ws)?
.into_iter()
.filter_map(|res| res.err())
.map(|res| anyhow!(res))
.collect::<Vec<anyhow::Error>>()
};
build_exit_error(errors)
}

fn run_for_workspace_root(args: ScriptsRunnerArgs, ws: &Workspace) -> Result<()> {
args.script
.map(|script| {
let script_definition = ws.script(&script).ok_or_else(|| {
missing_script_error(&script, "workspace root", " --workspace-root")
})?;
ops::execute_script(script_definition, &args.args, ws, ws.root(), None)
})
.unwrap_or_else(|| {
ws.config()
.ui()
.print(ScriptsList::for_workspace_root(ws.scripts().clone()));
Ok(())
})
}

fn run_for_packages(args: ScriptsRunnerArgs, ws: &Workspace) -> Result<Vec<Result<()>>> {
Ok(args
.packages_filter
.match_many(ws)?
.into_iter()
.map(|package| {
args.script
.clone()
.map(|script| run_script(script, &args.args, package.clone(), &ws))
.unwrap_or_else(|| list_scripts(package, &ws))
.map(|script| run_package_script(script, &args.args, package.clone(), ws))
.unwrap_or_else(|| {
ws.config().ui().print(ScriptsList::for_package(
package.id.name.clone(),
package.manifest.scripts.clone(),
ws.is_single_package(),
));
Ok(())
})
})
.filter_map(|res| res.err())
.map(|res| anyhow!(res))
.collect::<Vec<anyhow::Error>>();
.collect_vec())
}

fn build_exit_error(errors: Vec<anyhow::Error>) -> Result<()> {
if errors.is_empty() {
Ok(())
} else {
Expand All @@ -50,63 +90,132 @@ pub fn run(args: ScriptsRunnerArgs, config: &Config) -> Result<()> {
}
}

fn run_script(script: SmolStr, args: &[OsString], package: Package, ws: &Workspace) -> Result<()> {
fn run_package_script(
script: SmolStr,
args: &[OsString],
package: Package,
ws: &Workspace,
) -> Result<()> {
let script_definition = package.manifest.scripts.get(&script).ok_or_else(|| {
let package_name = package.id.name.to_string();
let package_selector = if !ws.is_single_package() {
format!(" -p {package_name}")
} else {
let package_selector = if ws.is_single_package() {
String::new()
} else {
format!(" -p {package_name}")
};
anyhow!(formatdoc! {r#"
missing script `{script}` for package: {package_name}
To see a list of scripts, run:
scarb run{package_selector}
"#})
missing_script_error(
&script,
&format!("package: {package_name}"),
&package_selector,
)
})?;
ops::execute_script(script_definition, args, ws, package.root(), None)
}

fn list_scripts(package: Package, ws: &Workspace) -> Result<()> {
let scripts = package
.manifest
.scripts
.iter()
.map(|(name, definition)| (name.to_string(), definition.to_string()))
.collect();
let package = package.id.name.to_string();
let single_package = ws.is_single_package();
ws.config().ui().print(ScriptsList {
scripts,
package,
single_package,
});
Ok(())
fn missing_script_error(script: &str, source: &str, selector: &str) -> anyhow::Error {
anyhow!(formatdoc! {r#"
missing script `{script}` for {source}
To see a list of scripts, run:
scarb run{selector}
"#})
}

#[derive(Serialize, Debug)]
struct ScriptsList {
struct PackageScriptsList {
package: String,
scripts: BTreeMap<String, String>,
single_package: bool,
}

impl Message for ScriptsList {
fn text(self) -> String {
#[derive(Serialize, Debug)]
struct WorkspaceScriptsList {
scripts: BTreeMap<String, String>,
}

#[derive(Serialize, Debug)]
#[serde(untagged)]
enum ScriptsList {
ForPackage(PackageScriptsList),
ForWorkspaceRoot(WorkspaceScriptsList),
}

impl ScriptsList {
pub fn for_package(
package: PackageName,
scripts: BTreeMap<SmolStr, ScriptDefinition>,
single_package: bool,
) -> Self {
let scripts = scripts
.iter()
.map(|(name, definition)| (name.to_string(), definition.to_string()))
.collect();
Self::ForPackage(PackageScriptsList {
package: package.to_string(),
scripts,
single_package,
})
}

pub fn for_workspace_root(scripts: BTreeMap<SmolStr, ScriptDefinition>) -> Self {
let scripts = scripts
.iter()
.map(|(name, definition)| (name.to_string(), definition.to_string()))
.collect();
Self::ForWorkspaceRoot(WorkspaceScriptsList { scripts })
}

fn scripts(&self) -> &BTreeMap<String, String> {
match self {
Self::ForPackage(p) => &p.scripts,
Self::ForWorkspaceRoot(w) => &w.scripts,
}
}
}

impl PackageScriptsList {
pub fn text(&self) -> String {
let mut text = String::new();
write!(text, "Scripts available via `scarb run`",).unwrap();
if !self.single_package {
write!(text, " for package `{}`", self.package).unwrap();
}
writeln!(text, ":",).unwrap();
for (name, definition) in self.scripts {
writeln!(text, "{:<22}: {}", name, definition).unwrap();
}
write!(text, "{}", write_scripts(&self.scripts)).unwrap();
text
}
}

impl WorkspaceScriptsList {
pub fn text(&self) -> String {
let mut text = String::new();
writeln!(
text,
"Scripts available via `scarb run` for workspace root:",
)
.unwrap();
write!(text, "{}", write_scripts(&self.scripts)).unwrap();
text
}
}

fn write_scripts(scripts: &BTreeMap<String, String>) -> String {
let mut text = String::new();
for (name, definition) in scripts {
writeln!(text, "{:<22}: {}", name, definition).unwrap();
}
text
}

impl Message for ScriptsList {
fn text(self) -> String {
match self {
Self::ForPackage(p) => p.text(),
Self::ForWorkspaceRoot(w) => w.text(),
}
}

fn structured<S: Serializer>(self, ser: S) -> Result<S::Ok, S::Error> {
self.scripts.serialize(ser)
self.scripts().serialize(ser)
}
}
15 changes: 14 additions & 1 deletion scarb/src/core/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ use anyhow::{anyhow, bail, Result};
use camino::{Utf8Path, Utf8PathBuf};
use itertools::Itertools;
use scarb_ui::args::PackagesSource;
use smol_str::SmolStr;

use crate::compiler::Profile;
use crate::core::config::Config;
use crate::core::package::Package;
use crate::core::{PackageId, Target};
use crate::core::{PackageId, ScriptDefinition, Target};
use crate::flock::Filesystem;
use crate::{DEFAULT_TARGET_DIR_NAME, LOCK_FILE_NAME, MANIFEST_FILE_NAME};

Expand All @@ -22,6 +23,7 @@ pub struct Workspace<'c> {
members: BTreeMap<PackageId, Package>,
manifest_path: Utf8PathBuf,
profiles: Vec<Profile>,
scripts: BTreeMap<SmolStr, ScriptDefinition>,
root_package: Option<PackageId>,
target_dir: Filesystem,
}
Expand All @@ -33,6 +35,7 @@ impl<'c> Workspace<'c> {
root_package: Option<PackageId>,
config: &'c Config,
profiles: Vec<Profile>,
scripts: BTreeMap<SmolStr, ScriptDefinition>,
) -> Result<Self> {
let targets = packages
.iter()
Expand All @@ -58,6 +61,7 @@ impl<'c> Workspace<'c> {
root_package,
target_dir,
members: packages,
scripts,
})
}

Expand All @@ -75,6 +79,7 @@ impl<'c> Workspace<'c> {
root_package,
config,
profiles,
BTreeMap::new(),
)
}

Expand Down Expand Up @@ -174,6 +179,14 @@ impl<'c> Workspace<'c> {
names.dedup();
names
}

pub fn scripts(&self) -> &BTreeMap<SmolStr, ScriptDefinition> {
&self.scripts
}

pub fn script(&self, name: &SmolStr) -> Option<&ScriptDefinition> {
self.scripts.get(name)
}
}

fn check_unique_targets(targets: &Vec<&Target>) -> Result<()> {
Expand Down
2 changes: 2 additions & 0 deletions scarb/src/ops/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ fn read_workspace_root<'c>(
.parent()
.expect("Manifest path must have parent.");

let scripts = workspace.scripts.unwrap_or_default();
// Read workspace members.
let mut packages = workspace
.members
Expand Down Expand Up @@ -137,6 +138,7 @@ fn read_workspace_root<'c>(
root_package,
config,
profiles,
scripts,
)
} else {
// Read single package workspace
Expand Down
Loading

0 comments on commit 776ab9e

Please sign in to comment.