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, #[arg(long)] compile_platform: Option, #[command(subcommand)] command: Option, } #[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, platform: Option, 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 = 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::) .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::) .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)); } }