harmony/harmony_composer/src/main.rs
Ian Letourneau ee02906ce9
All checks were successful
Run Check Script / check (push) Successful in 1m39s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 3m5s
fix(composer): spawn commands to allow interaction (#71)
Using `Command::output()` executes the command and wait for it to be finished before returning the output.
Though in some cases the user might need to interact with the CLI before continuing, which hangs the command execution.

Instead, using `Command::spawn()` allows to forward stdin/stdout to the parent process.

Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/71
Reviewed-by: johnride <jg@nationtech.io>
2025-07-01 21:08:19 +00:00

327 lines
9.8 KiB
Rust

use bollard::models::ContainerCreateBody;
use bollard::query_parameters::{
CreateContainerOptionsBuilder, ListContainersOptionsBuilder, LogsOptions,
RemoveContainerOptions, StartContainerOptions, WaitContainerOptions,
};
use bollard::secret::HostConfig;
use cargo_metadata::{Artifact, Message, MetadataCommand};
use clap::{Args, Parser, Subcommand};
use futures_util::StreamExt;
use log::info;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tokio::fs;
#[derive(Parser)]
#[command(version, about, long_about = None, flatten_help = true, propagate_version = true)]
struct GlobalArgs {
#[arg(long, default_value = "harmony")]
harmony_path: String,
#[arg(long)]
compile_method: Option<CompileMethod>,
#[arg(long)]
compile_platform: Option<String>,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand, Clone, Debug)]
enum Commands {
Check(CheckArgs),
Compile,
Deploy(DeployArgs),
All(AllArgs),
}
#[derive(Args, Clone, Debug)]
struct CheckArgs {
#[arg(long, default_value = "check.sh")]
check_script_path: String,
}
#[derive(Args, Clone, Debug)]
struct DeployArgs {
#[arg(long, default_value_t = false)]
staging: bool,
#[arg(long, default_value_t = false)]
prod: bool,
#[arg(long, default_value_t = false)]
smoke_test: bool,
}
#[derive(Args, Clone, Debug)]
struct AllArgs {
#[command(flatten)]
check: CheckArgs,
#[command(flatten)]
deploy: DeployArgs,
}
#[tokio::main]
async fn main() {
env_logger::init();
let cli_args = GlobalArgs::parse();
let harmony_path = Path::new(&cli_args.harmony_path)
.try_exists()
.expect("couldn't check if path exists");
let harmony_bin_path: PathBuf = match harmony_path {
true => {
compile_harmony(
cli_args.compile_method,
cli_args.compile_platform,
cli_args.harmony_path.clone(),
)
.await
}
false => todo!("implement autodetect code"),
};
match cli_args.command {
Some(command) => match command {
Commands::Check(args) => {
let check_script_str =
format!("{}/{}", cli_args.harmony_path, args.check_script_path);
let check_script = Path::new(&check_script_str);
match check_script
.try_exists()
.expect("couldn't check if path exists")
{
true => (),
false => todo!("implement couldn't find path logic"),
};
let check_output = Command::new(check_script)
.spawn()
.expect("failed to run check script")
.wait_with_output()
.unwrap();
info!(
"check stdout: {}, check stderr: {}",
String::from_utf8(check_output.stdout).expect("couldn't parse from utf8"),
String::from_utf8(check_output.stderr).expect("couldn't parse from utf8")
);
}
Commands::Deploy(args) => {
let deploy = if args.staging {
todo!("implement staging deployment")
} else if args.prod {
todo!("implement prod deployment")
} else {
Command::new(harmony_bin_path).arg("-y").arg("-a").spawn()
}
.expect("failed to run harmony deploy");
let deploy_output = deploy.wait_with_output().unwrap();
println!(
"deploy output: {}",
String::from_utf8(deploy_output.stdout).expect("couldn't parse from utf8")
);
}
Commands::All(_args) => todo!(
"take all previous match arms and turn them into separate functions, and call them all one after the other"
),
Commands::Compile => return,
},
None => todo!("run interactively, ask for info on CLI"),
}
}
#[derive(Clone, Debug, clap::ValueEnum)]
enum CompileMethod {
LocalCargo,
Docker,
}
async fn compile_harmony(
method: Option<CompileMethod>,
platform: Option<String>,
harmony_location: String,
) -> PathBuf {
let platform = match platform {
Some(p) => p,
None => current_platform::CURRENT_PLATFORM.to_string(),
};
let cargo_exists = Command::new("which")
.arg("cargo")
.status()
.expect("couldn't get `which cargo` status")
.success();
let method = match method {
Some(m) => m,
None => {
if cargo_exists {
return compile_cargo(platform, harmony_location).await;
} else {
return compile_docker(platform, harmony_location).await;
}
}
};
match method {
CompileMethod::LocalCargo => return compile_cargo(platform, harmony_location).await,
CompileMethod::Docker => return compile_docker(platform, harmony_location).await,
};
}
// TODO: make sure this works with cargo workspaces
async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf {
let metadata = MetadataCommand::new()
.manifest_path(format!("{}/Cargo.toml", harmony_location))
.exec()
.unwrap();
let mut cargo_build = Command::new("cargo")
.current_dir(&harmony_location)
.args(vec![
"build",
format!("--target={}", platform).as_str(),
"--release",
"--message-format=json-render-diagnostics",
])
.stdout(Stdio::piped())
.spawn()
.expect("run cargo command failed");
let mut artifacts: Vec<Artifact> = vec![];
let reader = std::io::BufReader::new(cargo_build.stdout.take().unwrap());
for message in cargo_metadata::Message::parse_stream(reader) {
match message.unwrap() {
Message::CompilerMessage(_msg) => (),
Message::CompilerArtifact(artifact) => {
if artifact.manifest_path
== metadata
.root_package()
.expect("failed to get root package")
.manifest_path
{
println!("{:?}", artifact);
artifacts.push(artifact);
}
}
Message::BuildScriptExecuted(_script) => (),
Message::BuildFinished(finished) => {
println!("{:?}", finished);
}
_ => (), // Unknown message
}
}
let bin = artifacts
.last()
.expect("no binaries built")
.filenames
.first()
.expect("couldn't get filename");
let bin_out;
if let Some(ext) = bin.extension() {
bin_out = PathBuf::from(format!("{}/harmony.{}", harmony_location, ext));
let _copy_res = fs::copy(&bin, &bin_out).await;
} else {
bin_out = PathBuf::from(format!("{}/harmony", harmony_location));
let _copy_res = fs::copy(&bin, &bin_out).await;
}
return bin_out;
}
async fn compile_docker(platform: String, harmony_location: String) -> PathBuf {
let docker_client =
bollard::Docker::connect_with_local_defaults().expect("couldn't connect to docker");
let mut filters = HashMap::new();
filters.insert(String::from("name"), vec![String::from("harmony_build")]);
let list_containers_options = ListContainersOptionsBuilder::new()
.all(true)
.filters(&filters)
.build();
let containers = &docker_client
.list_containers(Some(list_containers_options))
.await
.expect("list containers failed");
if containers.len() > 0 {
docker_client
.remove_container("harmony_build", None::<RemoveContainerOptions>)
.await
.expect("failed to remove container");
}
let options = Some(
CreateContainerOptionsBuilder::new()
.name("harmony_build")
.build(),
);
let config = ContainerCreateBody {
image: Some("hub.nationtech.io/harmony/harmony_composer".to_string()),
working_dir: Some("/mnt".to_string()),
cmd: Some(vec![
format!("--compile-platform={}", platform),
format!("--harmony-path=/mnt"),
"compile".to_string(),
]),
host_config: Some(HostConfig {
binds: Some(vec![format!("{}:/mnt", harmony_location)]),
..Default::default()
}),
..Default::default()
};
docker_client
.create_container(options, config)
.await
.expect("couldn't create container");
docker_client
.start_container("harmony_build", None::<StartContainerOptions>)
.await
.expect("couldn't start container");
let wait_options = WaitContainerOptions {
condition: "not-running".to_string(),
};
let mut wait = docker_client
.wait_container("harmony_build", Some(wait_options))
.boxed();
let mut logs_stream = docker_client.logs(
"harmony_build",
Some(LogsOptions {
follow: true,
stdout: true,
stderr: true,
tail: "all".to_string(),
..Default::default()
}),
);
while let Some(l) = logs_stream.next().await {
let l_str = l.expect("couldn't unwrap logoutput").to_string();
println!("{}", l_str);
}
// wait until container is no longer running
while let Some(_) = wait.next().await {}
// hack that should be cleaned up
if platform.contains("windows") {
return PathBuf::from(format!("{}/harmony.exe", harmony_location));
} else {
return PathBuf::from(format!("{}/harmony", harmony_location));
}
}