zellij/xtask/src/pipelines.rs
har7an 8031d6bf64
xtask/pipeline: Fix publish task (#2711)
* xtask/pipeline: Fix publish task

which was previously stuck in an infinite loop after successfully
publishing a crate. The error originated in the code only checking for
error conditions but not breaking out of the inner infinite loop in case
of success.

* xtask: Improve publish failure UX

by offering the user more actions to choose from when an error occured.

* utils/assets: Add generated prost files to assets

to make sure they're available at build time and are picked up by all
components. It seems we hit some strange bug with the build script
where, when running `cargo publish --dry-run` the build script **is
not** run before regularly compiling zellij-utils. This shouldn't happen
according to the docs, but I cannot explain what's causing it. So we're
using this as a workaround for now to make a smooth release.

* xtask: Prevent accidental git commit deletion

when dry-running a publish.

* utils: Add comments to protobuf-related code

to explain why these changes were performed. The comments all include a
link to an issue comment explaining the situation in greater detail.

* xtask: Build protobuf definitions

when building any part of the project, similar to how we build the
plugins when required. This should ensure that all crates built through
`cargo xtask` (which is the officially supported build method) will
receive up-to-date protobuf definitions.
2023-08-28 06:24:27 +00:00

405 lines
13 KiB
Rust

//! Composite pipelines for the build system.
//!
//! Defines multiple "pipelines" that run specific individual steps in sequence.
use crate::{build, clippy, format, test};
use crate::{flags, WorkspaceMember};
use anyhow::Context;
use xshell::{cmd, Shell};
/// Perform a default build.
///
/// Runs the following steps in sequence:
///
/// - format
/// - build
/// - test
/// - clippy
pub fn make(sh: &Shell, flags: flags::Make) -> anyhow::Result<()> {
let err_context = || format!("failed to run pipeline 'make' with args {flags:?}");
if flags.clean {
crate::cargo()
.and_then(|cargo| cmd!(sh, "{cargo} clean").run().map_err(anyhow::Error::new))
.with_context(err_context)?;
}
format::format(sh, flags::Format { check: false })
.and_then(|_| {
build::build(
sh,
flags::Build {
release: flags.release,
no_plugins: false,
plugins_only: false,
},
)
})
.and_then(|_| test::test(sh, flags::Test { args: vec![] }))
.and_then(|_| clippy::clippy(sh, flags::Clippy {}))
.with_context(err_context)
}
/// Generate a runnable executable.
///
/// Runs the following steps in sequence:
///
/// - [`build`](build::build) (release, plugins only)
/// - [`build`](build::build) (release, without plugins)
/// - [`manpage`](build::manpage)
/// - Copy the executable to [target file](flags::Install::destination)
pub fn install(sh: &Shell, flags: flags::Install) -> anyhow::Result<()> {
let err_context = || format!("failed to run pipeline 'install' with args {flags:?}");
// Build and optimize plugins
build::build(
sh,
flags::Build {
release: true,
no_plugins: false,
plugins_only: true,
},
)
.and_then(|_| {
// Build the main executable
build::build(
sh,
flags::Build {
release: true,
no_plugins: true,
plugins_only: false,
},
)
})
.and_then(|_| {
// Generate man page
build::manpage(sh)
})
.with_context(err_context)?;
// Copy binary to destination
let destination = if flags.destination.is_absolute() {
flags.destination.clone()
} else {
std::env::current_dir()
.context("Can't determine current working directory")?
.join(&flags.destination)
};
sh.change_dir(crate::project_root());
sh.copy_file("target/release/zellij", &destination)
.with_context(err_context)
}
/// Run zellij debug build.
pub fn run(sh: &Shell, mut flags: flags::Run) -> anyhow::Result<()> {
let err_context =
|flags: &flags::Run| format!("failed to run pipeline 'run' with args {:?}", flags);
let singlepass = flags.singlepass.then_some(["--features", "singlepass"]);
if flags.quick_run {
if flags.data_dir.is_some() {
eprintln!("cannot use '--data-dir' and '--quick-run' at the same time!");
std::process::exit(1);
}
flags.data_dir.replace(crate::asset_dir());
}
let profile = if flags.disable_deps_optimize {
"dev"
} else {
"dev-opt"
};
if let Some(ref data_dir) = flags.data_dir {
let data_dir = sh.current_dir().join(data_dir);
crate::cargo()
.and_then(|cargo| {
cmd!(sh, "{cargo} run")
.args(["--package", "zellij"])
.arg("--no-default-features")
.args(["--features", "disable_automatic_asset_installation"])
.args(singlepass.iter().flatten())
.args(["--profile", profile])
.args(["--", "--data-dir", &format!("{}", data_dir.display())])
.args(&flags.args)
.run()
.map_err(anyhow::Error::new)
})
.with_context(|| err_context(&flags))
} else {
build::build(
sh,
flags::Build {
release: false,
no_plugins: false,
plugins_only: true,
},
)
.and_then(|_| crate::cargo())
.and_then(|cargo| {
cmd!(sh, "{cargo} run")
.args(singlepass.iter().flatten())
.args(["--profile", profile])
.args(["--"])
.args(&flags.args)
.run()
.map_err(anyhow::Error::new)
})
.with_context(|| err_context(&flags))
}
}
/// Bundle all distributable content to `target/dist`.
///
/// This includes the optimized zellij executable from the [`install`] pipeline, the man page, the
/// `.desktop` file and the application logo.
pub fn dist(sh: &Shell, _flags: flags::Dist) -> anyhow::Result<()> {
let err_context = || "failed to run pipeline 'dist'";
sh.change_dir(crate::project_root());
if sh.path_exists("target/dist") {
sh.remove_path("target/dist").with_context(err_context)?;
}
sh.create_dir("target/dist")
.map_err(anyhow::Error::new)
.and_then(|_| {
install(
sh,
flags::Install {
destination: crate::project_root().join("./target/dist/zellij"),
},
)
})
.with_context(err_context)?;
sh.create_dir("target/dist/man")
.and_then(|_| sh.copy_file("assets/man/zellij.1", "target/dist/man/zellij.1"))
.and_then(|_| sh.copy_file("assets/zellij.desktop", "target/dist/zellij.desktop"))
.and_then(|_| sh.copy_file("assets/logo.png", "target/dist/logo.png"))
.with_context(err_context)
}
/// Actions for the user to choose from to resolve publishing errors/conflicts.
enum UserAction {
Retry,
Abort,
Ignore,
}
/// Make a zellij release and publish all crates.
pub fn publish(sh: &Shell, flags: flags::Publish) -> anyhow::Result<()> {
let err_context = "failed to publish zellij";
// Process flags
let dry_run = if flags.dry_run {
Some("--dry-run")
} else {
None
};
let remote = flags.git_remote.unwrap_or("origin".into());
let registry = if let Some(registry) = flags.cargo_registry {
Some(format!(
"--registry={}",
registry
.into_string()
.map_err(|registry| anyhow::Error::msg(format!(
"failed to convert '{:?}' to valid registry name",
registry
)))
.context(err_context)?
))
} else {
None
};
let registry = registry.as_ref();
sh.change_dir(crate::project_root());
let cargo = crate::cargo().context(err_context)?;
let project_dir = crate::project_root();
let manifest = sh
.read_file(project_dir.join("Cargo.toml"))
.context(err_context)?
.parse::<toml::Value>()
.context(err_context)?;
// Version of the core crate
let version = manifest
.get("package")
.and_then(|package| package["version"].as_str())
.context(err_context)?;
let mut skip_build = false;
if cmd!(sh, "git tag -l")
.read()
.context(err_context)?
.contains(version)
{
println!();
println!("Git tag 'v{version}' is already present.");
println!("If this is a mistake, delete it with: git tag -d 'v{version}'");
println!("Skip build phase and continue to publish? [y/n]");
let stdin = std::io::stdin();
loop {
let mut buffer = String::new();
stdin.read_line(&mut buffer).context(err_context)?;
match buffer.trim_end() {
"y" | "Y" => {
skip_build = true;
break;
},
"n" | "N" => {
skip_build = false;
break;
},
_ => {
println!(" --> Unknown input '{buffer}', ignoring...");
println!();
println!("Skip build phase and continue to publish? [y/n]");
},
}
}
}
if !skip_build {
// Clean project
cmd!(sh, "{cargo} clean").run().context(err_context)?;
// Build plugins
build::build(
sh,
flags::Build {
release: true,
no_plugins: false,
plugins_only: true,
},
)
.context(err_context)?;
// Update default config
sh.copy_file(
project_dir
.join("zellij-utils")
.join("assets")
.join("config")
.join("default.kdl"),
project_dir.join("example").join("default.kdl"),
)
.context(err_context)?;
// Commit changes
cmd!(sh, "git commit -aem")
.arg(format!("chore(release): v{}", version))
.run()
.context(err_context)?;
// Tag release
cmd!(sh, "git tag --annotate --message")
.arg(format!("Version {}", version))
.arg(format!("v{}", version))
.run()
.context(err_context)?;
}
let closure = || -> anyhow::Result<()> {
// Push commit and tag
if flags.dry_run {
println!("Skipping push due to dry-run");
} else {
cmd!(sh, "git push --atomic {remote} main v{version}")
.run()
.context(err_context)?;
}
// Publish all the crates
for WorkspaceMember { crate_name, .. } in crate::WORKSPACE_MEMBERS.iter() {
if crate_name.contains("plugin") || crate_name.contains("xtask") {
continue;
}
let _pd = sh.push_dir(project_dir.join(crate_name));
loop {
let msg = format!(">> Publishing '{crate_name}'");
crate::status(&msg);
println!("{}", msg);
let more_args = match *crate_name {
// This is needed for zellij to pick up the plugins from the assets included in
// the released zellij-utils binary
"." => Some("--no-default-features"),
_ => None,
};
if let Err(err) = cmd!(
sh,
"{cargo} publish {registry...} {more_args...} {dry_run...}"
)
.run()
.context(err_context)
{
println!();
println!("Publishing crate '{crate_name}' failed with error:");
println!("{:?}", err);
println!();
println!("Please choose what to do: [r]etry/[a]bort/[i]gnore");
let stdin = std::io::stdin();
let action;
loop {
let mut buffer = String::new();
stdin.read_line(&mut buffer).context(err_context)?;
match buffer.trim_end() {
"r" | "R" => {
action = UserAction::Retry;
break;
},
"a" | "A" => {
action = UserAction::Abort;
break;
},
"i" | "I" => {
action = UserAction::Ignore;
break;
},
_ => {
println!(" --> Unknown input '{buffer}', ignoring...");
println!();
println!("Please choose what to do: [r]etry/[a]bort/[i]gnore");
},
}
}
match action {
UserAction::Retry => continue,
UserAction::Ignore => break,
UserAction::Abort => {
eprintln!("Aborting publish for crate '{crate_name}'");
return Err::<(), _>(err);
},
}
} else {
// publish successful, continue to next crate
break;
}
}
}
println!();
println!(" +-----------------------------------------------+");
println!(" | PRAISE THE DEVS, WE HAVE A NEW ZELLIJ RELEASE |");
println!(" +-----------------------------------------------+");
Ok(())
};
// We run this in a closure so that a failure in any of the commands doesn't abort the whole
// program. When dry-running we need to undo the release commit first!
let result = closure();
if flags.dry_run && !skip_build {
cmd!(sh, "git reset --hard HEAD~1")
.run()
.context(err_context)?;
}
result
}