use {
crate::{
artifact::FileModeArgParser,
cli::{Command, Config},
content::{ContentProvider, ContentProviderGuard},
manifest::Manifest,
},
anyhow::{anyhow, Result},
clap::{Args, Parser},
std::{
env,
io::Write,
num::NonZero,
path::Path,
process::{Command as ProcessCommand, Stdio},
},
};
#[derive(Args, Clone)]
struct EditOptions {
#[arg(short = 's', long = "spec", value_name = "SPEC", global = true)]
spec: Option<String>,
#[arg(short = 'e', long = "editor", value_name = "EDITOR", global = true)]
edit: Option<String>,
#[arg(long = "no-verify", display_order = 0, action)]
insecure_release: bool,
}
#[derive(Parser)]
#[command(about = "Edit the manifest or spec metadata")]
pub struct Edit {
#[command(flatten)]
opts: EditOptions,
#[command(subcommand)]
cmd: Option<EditCommands>,
}
#[derive(Parser)]
enum EditCommands {
Env(EditEnv),
Script(EditScript),
Artifact(EditArtifact),
}
#[derive(Parser)]
#[command(about = "Edit build environment variables for a spec")]
struct EditEnv;
#[derive(Parser)]
#[command(about = "Edit build script for a spec")]
struct EditScript;
#[derive(Parser)]
#[command(about = "Edit inline text artifact content")]
struct EditArtifact {
#[arg(value_name = "NAME")]
name: String,
#[arg(long = "target", value_name = "TARGET_PATH")]
target: String,
#[arg(long = "mode", value_name = "MODE", value_parser = FileModeArgParser)]
mode: Option<NonZero<u32>>,
#[arg(long = "only-arch", value_name = "ARCH")]
target_arch: Option<String>,
#[arg(long = "stage", action)]
stage: bool,
}
impl<C: Config> Command<C> for Edit {
fn exec(&self, conf: &C) -> Result<()> {
let cmd = self.cmd.as_ref();
let spec = self.opts.spec.as_deref();
smol::block_on(async move {
if cmd.is_none() {
let editor = EditorCommand::resolve(self.opts.edit.as_deref())?;
return edit_manifest(conf, self.opts.insecure_release, &editor).await;
}
let cmd = cmd.unwrap();
let (mut manifest, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), conf.fetcher()?).await?;
if !has_valid_lock {
return Err(anyhow!("manifest lock is not live; run update first"));
}
let editor = EditorCommand::resolve(self.opts.edit.as_deref())?;
match cmd {
EditCommands::Env(_) => edit_env(&mut manifest, &editor, spec).await?,
EditCommands::Script(_) => edit_script(&mut manifest, &editor, spec).await?,
EditCommands::Artifact(artifact) => {
edit_artifact(&mut manifest, &editor, spec, artifact).await?;
}
}
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
manifest.resolve(conf.concurrency(), fetcher).await?;
manifest.store().await?;
guard.commit().await?;
Ok(())
})
}
}
struct EditorCommand {
program: String,
args: Vec<String>,
}
impl EditorCommand {
fn resolve(edit: Option<&str>) -> Result<Self> {
let editor = edit
.map(|value| value.to_string())
.or_else(|| env::var("VISUAL").ok())
.or_else(|| env::var("EDITOR").ok())
.or_else(|| {
std::fs::metadata("/usr/bin/editor")
.map(|md| md.is_file())
.is_ok()
.then(|| "/usr/bin/editor".to_string())
})
.ok_or_else(|| anyhow!("editor not configured and /usr/bin/editor is absent: set $VISUAL/$EDITOR or pass --edit"))?;
let mut parts = editor.split_whitespace();
let program = parts
.next()
.ok_or_else(|| anyhow!("editor command is empty"))?
.to_string();
let args = parts.map(|part| part.to_string()).collect::<Vec<_>>();
Ok(Self { program, args })
}
fn run(&self, path: &Path) -> Result<()> {
let status = ProcessCommand::new(&self.program)
.args(&self.args)
.arg(path)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
if !status.success() {
return Err(anyhow!("editor exited with status {status}"));
}
Ok(())
}
}
async fn edit_manifest<C: Config>(
conf: &C,
insecure_release: bool,
editor: &EditorCommand,
) -> Result<()> {
let path = smol::fs::canonicalize(conf.manifest()).await?;
editor.run(&path)?;
let fetcher = conf.fetcher()?;
let (mut mf, has_valid_lock) = Manifest::from_file(conf.manifest(), conf.arch(), fetcher)
.await
.map_err(|err| {
anyhow!("failed to load manifest after editing: {err}; manifest may be malformed",)
})?;
if has_valid_lock {
return Ok(());
}
let guard = fetcher.init().await?;
mf.update(false, false, insecure_release, conf.concurrency(), fetcher)
.await
.map_err(|err| {
anyhow!("failed to update manifest after editing: {err}; manifest may be malformed",)
})?;
mf.resolve(conf.concurrency(), fetcher)
.await
.map_err(|err| {
anyhow!("failed to resolve manifest after editing: {err}; manifest may be malformed",)
})?;
mf.store().await.map_err(|err| {
anyhow!("failed to store manifest after editing: {err}; manifest may be malformed",)
})?;
guard.commit().await?;
Ok(())
}
async fn edit_env(
manifest: &mut Manifest,
editor: &EditorCommand,
spec: Option<&str>,
) -> Result<()> {
let mut tmp = tempfile::Builder::new().suffix(".env").tempfile()?;
write!(tmp, "{}", manifest.lookup_spec(spec)?.env_block()?)?;
tmp.flush()?;
editor.run(tmp.path())?;
let contents = std::fs::read_to_string(tmp.path())?;
manifest.spec_update_env_block(spec, contents)?;
Ok(())
}
async fn edit_script(
manifest: &mut Manifest,
editor: &EditorCommand,
spec: Option<&str>,
) -> Result<()> {
let script = manifest
.lookup_spec(spec)?
.build_script()
.map(str::to_string);
let mut tmp = tempfile::Builder::new().suffix(".sh").tempfile()?;
if let Some(script) = script {
write!(tmp, "{script}")?;
}
tmp.flush()?;
editor.run(tmp.path())?;
let contents = std::fs::read_to_string(tmp.path())?;
let script = if contents.trim().is_empty() {
None
} else {
Some(contents)
};
manifest.set_build_script(spec, script)?;
Ok(())
}
async fn edit_artifact(
manifest: &mut Manifest,
editor: &EditorCommand,
spec: Option<&str>,
artifact: &EditArtifact,
) -> Result<()> {
let mut existing_text = None;
let mut existing_mode = None;
let mut existing_arch = None;
if let Some(existing) = manifest.artifact(&artifact.name) {
let existing = existing
.as_text()
.ok_or_else(|| anyhow!("artifact {} exists but is not text", artifact.name.as_str()))?;
existing_text = Some(existing.text().to_string());
existing_mode = existing.mode();
existing_arch = existing.arch().map(str::to_string);
}
let mut tmp = tempfile::Builder::new().suffix(".txt").tempfile()?;
if let Some(text) = existing_text {
write!(tmp, "{text}")?;
}
tmp.flush()?;
editor.run(tmp.path())?;
let contents = std::fs::read_to_string(tmp.path())?;
let mode = artifact.mode.or(existing_mode);
let arch = artifact.target_arch.clone().or(existing_arch);
manifest.upsert_text_artifact(
&artifact.name,
artifact.target.clone(),
contents,
mode,
arch,
)?;
if artifact.stage {
manifest.add_stage_items(spec, vec![artifact.name.clone()], None)?;
}
Ok(())
}