Stop using cargo internal API, too complicated. Switch to programmatically parsing metadata output. Add CI (untested, probably doesn't work), create dockerfile

This commit is contained in:
Taha Hawa 2025-06-14 19:21:05 -04:00 committed by tahahawa
parent 99e9aad687
commit 6191fd1858
6 changed files with 165 additions and 1908 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
target/

View File

@ -0,0 +1,46 @@
name: Compile and package harmony_composer
on:
push:
branches:
- main
jobs:
check:
runs-on: rust-cargo
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build for Linux x86_64
run: cargo build --release --bin harmony_composer --target x86_64-unknown-linux-gnu
- name: Build for Linux ARM64
run: cargo build --release --bin harmony_composer --target aarch64-unknown-linux-gnu
- name: Build for Windows x86_64 GNU
run: cargo build --release --bin harmony_composer --target x86_64-pc-windows-gnu
# - name: Build for MacOS ARM64
# run: cargo build --release --bin harmony_composer --target aarch64-apple-darwin
- uses: actions/upload-artifact@v4
with:
name: binaries
path: |
target/x86_64-unknown-linux-gnu/release/harmony_composer
target/aarch64-unknown-linux-gnu/release/harmony_composer
target/x86_64-pc-windows-gnu/release/harmony_composer.exe
- name: Setup log into hub.nationtech.io
uses: docker/login-action@v2
with:
registry: hub.nationtech.io
username: ${{ secrets.HUB_BOT_USERNAME }}
password: ${{ secrets.HUB_BOT_PASSWORD }}
# TODO: build ARM images too
- name: Build docker container
run: docker build . -t hub.nationtech.io/harmony/harmony_composer
- name: Push docker container
run: docker push hub.nationtech.io/harmony/harmony_composer

1859
Cargo.lock generated

File diff suppressed because it is too large Load Diff

15
Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM docker.io/rust:1.87.0 AS build
WORKDIR /app
COPY . .
RUN cargo build --release --bin harmony_composer
FROM docker.io/rust:1.87.0
WORKDIR /app
COPY --from=build /app/target/release/harmony_composer .
ENTRYPOINT ["/app/harmony_composer"]

View File

@ -6,14 +6,12 @@ readme.workspace = true
license.workspace = true license.workspace = true
[dependencies] [dependencies]
assert_cmd = "2.0.17"
clap = { version = "4.5.35", features = ["derive"] } clap = { version = "4.5.35", features = ["derive"] }
inquire.workspace = true
tokio.workspace = true tokio.workspace = true
env_logger.workspace = true env_logger.workspace = true
log.workspace = true log.workspace = true
cargo = "0.88.0"
bollard = "0.19.0" bollard = "0.19.0"
current_platform = "0.2.0" current_platform = "0.2.0"
futures-util = "0.3.31" futures-util = "0.3.31"
serde_json = "1.0.140" serde_json = "1.0.140"
cargo_metadata = "0.20.0"

View File

@ -4,13 +4,14 @@ use bollard::query_parameters::{
RemoveContainerOptions, StartContainerOptions, WaitContainerOptions, RemoveContainerOptions, StartContainerOptions, WaitContainerOptions,
}; };
use bollard::secret::HostConfig; use bollard::secret::HostConfig;
use cargo_metadata::{Message, MetadataCommand};
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use futures_util::{StreamExt, TryStreamExt}; use futures_util::{StreamExt, TryStreamExt};
use log::info; use log::info;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::format; use std::fmt::format;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::{Command, Stdio};
use tokio::fs; use tokio::fs;
#[derive(Parser)] #[derive(Parser)]
@ -32,6 +33,7 @@ struct GlobalArgs {
#[derive(Subcommand, Clone, Debug)] #[derive(Subcommand, Clone, Debug)]
enum Commands { enum Commands {
Check(CheckArgs), Check(CheckArgs),
Compile,
Deploy(DeployArgs), Deploy(DeployArgs),
All(AllArgs), All(AllArgs),
} }
@ -97,9 +99,7 @@ async fn main() {
false => todo!("implement couldn't find path logic"), false => todo!("implement couldn't find path logic"),
}; };
let check_output = Command::new("sh") let check_output = Command::new(check_script)
.arg("-c")
.arg(check_script)
.output() .output()
.expect("failed to run check script"); .expect("failed to run check script");
info!( info!(
@ -129,6 +129,7 @@ async fn main() {
Commands::All(_args) => todo!( Commands::All(_args) => todo!(
"take all previous match arms and turn them into separate functions, and call them all one after the other" "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"), None => todo!("run interactively, ask for info on CLI"),
} }
@ -150,14 +151,17 @@ async fn compile_harmony(
None => current_platform::CURRENT_PLATFORM.to_string(), None => current_platform::CURRENT_PLATFORM.to_string(),
}; };
let gctx = cargo::util::context::GlobalContext::default().unwrap(); let cargo_exists = Command::new("which")
let cargo_exists = gctx.cargo_exe().unwrap().exists(); .arg("cargo")
.status()
.expect("couldn't get `which cargo` status")
.success();
let method = match method { let method = match method {
Some(m) => m, Some(m) => m,
None => { None => {
if cargo_exists { if cargo_exists {
return compile_cargo(gctx, harmony_location).await; return compile_cargo(platform, harmony_location).await;
} else { } else {
return compile_docker(platform, harmony_location).await; return compile_docker(platform, harmony_location).await;
} }
@ -165,39 +169,64 @@ async fn compile_harmony(
}; };
match method { match method {
CompileMethod::LocalCargo => return compile_cargo(gctx, harmony_location).await, CompileMethod::LocalCargo => return compile_cargo(platform, harmony_location).await,
CompileMethod::Docker => return compile_docker(platform, harmony_location).await, CompileMethod::Docker => return compile_docker(platform, harmony_location).await,
}; };
} }
async fn compile_cargo( async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf {
gctx: cargo::util::context::GlobalContext, let metadata = MetadataCommand::new()
harmony_location: String, .manifest_path(format!("{}/Cargo.toml", harmony_location))
) -> PathBuf { .exec()
let cargo_build = cargo::ops::compile( .unwrap();
&cargo::core::Workspace::new(
&Path::new(&format!("{}/Cargo.toml", harmony_location))
.canonicalize()
.expect("Couldn't find Cargo.toml in harmony dir"),
&gctx,
)
.unwrap(),
&cargo::ops::CompileOptions::new(&gctx, cargo::core::compiler::CompileMode::Build).unwrap(),
)
.expect("build didn't go successfully");
let bin = cargo_build.binaries.first().expect("no binaries built"); 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 bin = cargo_metadata::camino::Utf8PathBuf::new();
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) => {
// println!("{:?}", artifact);
if artifact.package_id.eq(&metadata
.root_package()
.expect("failed to get root package")
.id)
{
bin = artifact
.filenames
.first()
.expect("couldn't get artifact filename")
.clone()
}
}
Message::BuildScriptExecuted(_script) => (),
Message::BuildFinished(finished) => {
println!("{:?}", finished);
}
_ => (), // Unknown message
}
}
// let bin = cargo_build.binaries.first().expect("no binaries built");
let bin_out; let bin_out;
if let Some(ext) = bin.path.extension() { if let Some(ext) = bin.extension() {
bin_out = PathBuf::from(format!( bin_out = PathBuf::from(format!("{}/harmony.{}", harmony_location, ext));
"{}/harmony.{}", let _copy_res = fs::copy(&bin, &bin_out).await;
harmony_location,
ext.to_str().expect("couldn't convert OsStr to str")
));
let _copy_res = fs::copy(bin.path.clone(), bin_out.clone()).await;
} else { } else {
bin_out = PathBuf::from(format!("{}/harmony", harmony_location)); bin_out = PathBuf::from(format!("{}/harmony", harmony_location));
let _copy_res = fs::copy(bin.path.clone(), bin_out.clone()).await; let _copy_res = fs::copy(&bin, &bin_out).await;
} }
return bin_out; return bin_out;
} }
@ -232,14 +261,12 @@ async fn compile_docker(platform: String, harmony_location: String) -> PathBuf {
); );
let config = ContainerCreateBody { let config = ContainerCreateBody {
image: Some("docker.io/rust:1.87.0".to_string()), image: Some("hub.nationtech.io/harmony/harmony_composer".to_string()),
working_dir: Some("/mnt".to_string()), working_dir: Some("/mnt".to_string()),
entrypoint: Some(vec![ cmd: Some(vec![
"cargo".to_string(), format!("--compile-platform={}", platform),
"build".to_string(), format!("--harmony-path=/mnt"),
format!("--target={}", platform), "compile".to_string(),
"--release".to_string(),
"--message-format=json".to_string(),
]), ]),
host_config: Some(HostConfig { host_config: Some(HostConfig {
binds: Some(vec![format!("{}:/mnt", harmony_location)]), binds: Some(vec![format!("{}:/mnt", harmony_location)]),
@ -276,49 +303,18 @@ async fn compile_docker(platform: String, harmony_location: String) -> PathBuf {
}), }),
); );
let mut bin_out = PathBuf::from(format!("{}/harmony", harmony_location));
while let Some(l) = logs_stream.next().await { while let Some(l) = logs_stream.next().await {
let l_str = l.expect("couldn't unwrap logoutput").to_string(); let l_str = l.expect("couldn't unwrap logoutput").to_string();
println!("{}", l_str); println!("{}", l_str);
let l_json: Option<serde_json::Value> =
serde_json::from_str(l_str.as_str()).unwrap_or(None);
match l_json {
Some(j) => match j.get("manifest_path") {
Some(p) => {
if p == "/mnt/Cargo.toml" {
let bin = PathBuf::from(format!(
"{}/{}",
harmony_location,
j.get("executable")
.expect("couldn't get json executable")
.as_str()
.expect("couldn't get json executable as str")
.replacen("/mnt/", "", 1)
));
if let Some(ext) = bin.extension() {
bin_out = PathBuf::from(format!(
"{}/harmony.{}",
harmony_location,
ext.to_str().expect("couldn't convert OsStr to str")
));
let _copy_res = fs::copy(bin.clone(), bin_out.clone()).await;
} else {
bin_out = PathBuf::from(format!("{}/harmony", harmony_location));
let _copy_res = fs::copy(bin.clone(), bin_out.clone()).await;
}
}
}
None => (),
},
None => (),
};
} }
// wait until container is no longer running // wait until container is no longer running
while let Some(_) = wait.next().await {} while let Some(_) = wait.next().await {}
return bin_out; // 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));
}
} }