feat: add shared Rust MSIX engine and CLI

Closes #2
This commit is contained in:
jychoi4 committed 2026-11-02 13:44:00 +09:00
1 parent 24b624301c
commit 6213fec7d2
29 files changed
+4058 -5

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
[package]
name = "package-builder"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
schemars.workspace = true
package-domain.workspace = true
package-engine.workspace = true
package-infra.workspace = true
clap = { version = "4", features = ["derive"] }
ctrlc = "3"
+144
View File
@@ -0,0 +1,144 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use package_infra::{self as infra, MsixBackend};
use std::{
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
#[derive(Parser)]
#[command(
version,
about = "Installer Studio — shared Rust package engine (development builds)"
)]
struct Cli {
#[command(subcommand)]
command: Action,
}
#[derive(Subcommand)]
enum Action {
Init {
#[arg(long)]
project: PathBuf,
},
Validate {
#[arg(long)]
project: PathBuf,
},
Plan {
#[command(subcommand)]
command: PlanAction,
},
Build {
#[arg(long)]
project: PathBuf,
#[arg(long)]
plan_id: String,
},
Job {
#[command(subcommand)]
command: JobAction,
},
Export {
#[arg(long)]
project: PathBuf,
#[arg(long)]
release: String,
#[arg(long)]
output: PathBuf,
},
Schema,
}
#[derive(Subcommand)]
enum PlanAction {
Release {
#[arg(long)]
project: PathBuf,
#[arg(long)]
target: String,
},
}
#[derive(Subcommand)]
enum JobAction {
Status {
#[arg(long)]
project: PathBuf,
#[arg(long)]
id: String,
},
}
fn print(value: &impl serde::Serialize) -> Result<()> {
println!("{}", serde_json::to_string_pretty(value)?);
Ok(())
}
fn run(action: Action, cancel: &AtomicBool) -> Result<i32> {
match action {
Action::Init { project } => print(&infra::create_project(&std::path::absolute(project)?)?)?,
Action::Validate { project } => {
let doc = infra::open_project(&project)?;
let report = package_engine::validate(
&MsixBackend,
&doc.project,
&doc.local,
doc.path.parent().unwrap(),
);
print(&report)?;
return Ok(if report.diagnostics.is_empty() { 0 } else { 2 });
}
Action::Plan {
command: PlanAction::Release { project, target },
} => print(&infra::plan_release(&project, &target)?)?,
Action::Build { project, plan_id } => print(&infra::build(&project, &plan_id, cancel)?)?,
Action::Job {
command: JobAction::Status { project, id },
} => print(&infra::job_status(&project, &id)?)?,
Action::Export {
project,
release,
output,
} => print(&infra::export_release(
&project,
&release,
&std::path::absolute(output)?,
)?)?,
Action::Schema => print(&schemars::schema_for!(package_domain::Project))?,
}
Ok(0)
}
fn main() {
let cli = Cli::parse();
let cancel = Arc::new(AtomicBool::new(false));
let signal = cancel.clone();
if let Err(error) = ctrlc::set_handler(move || signal.store(true, Ordering::Relaxed)) {
eprintln!("{error}");
std::process::exit(4);
}
let code = match run(cli.command, &cancel) {
Ok(code) => code,
Err(error) => {
let message = format!("{error:#}");
let code = if message.starts_with("Cancelled") {
130
} else if message.starts_with("JobLocked") {
6
} else if message.starts_with("Tool") {
4
} else if message.starts_with("ArtifactMismatch")
|| message.starts_with("IdentityMismatch")
{
5
} else {
2
};
eprintln!(
"{}",
serde_json::json!({ "code": code, "message": message })
);
code
}
};
std::process::exit(code);
}