use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use crate::meta::Project;
use crate::targets::{Target, TargetKind};
use crate::term::{HEADER, LOG_ERR, LOG_OUT};
pub struct BuildOutcome {
pub target: &'static str,
pub artifact: PathBuf,
pub seconds: f64,
}
fn cargo_dir(project: &Project, target: &Target, profile: &str) -> PathBuf {
project
.root
.join("build/day/cargo")
.join(target.name)
.join(profile)
}
pub fn status(prefix: &str, msg: &str) {
anstream::eprintln!("{HEADER}{prefix:>12}{HEADER:#} {msg}");
}
pub fn feature_selection(project: &Project, backend: &str) -> String {
let mut features = vec![backend.to_string()];
features.extend(crate::pieces::feature_union(project, backend));
features.join(",")
}
pub fn build(
project: &Project,
target: &'static Target,
profile: &str,
) -> Result<BuildOutcome, String> {
let host = crate::targets::host_os();
if target.host != "any" && target.host != host {
return Err(format!(
"target {} builds on a {} host (this is {})",
target.name, target.host, host
));
}
let start = std::time::Instant::now();
if let Err(e) = crate::resources::stage(project, target) {
status("Warning", &format!("resource staging skipped ({e})"));
}
match target.kind {
TargetKind::Desktop => {
let mut cmd = Command::new("cargo");
cmd.current_dir(&project.root)
.env("CARGO_TARGET_DIR", cargo_dir(project, target, profile));
let features = feature_selection(project, target.toolkit);
if target.toolkit == "winui" {
let manifest = write_winui_manifest(project, target, profile)?;
cmd.args(["rustc", "--bin", &project.manifest.app.name])
.args(["--no-default-features", "--features", &features]);
if profile == "release" {
cmd.arg("--release");
}
cmd.arg("--");
cmd.arg("-Clink-arg=/MANIFEST:EMBED");
cmd.arg(format!("-Clink-arg=/MANIFESTINPUT:{}", manifest.display()));
} else {
cmd.args([
"build",
"-p",
&project.manifest.app.name,
"--no-default-features",
])
.args(["--features", &features]);
if profile == "release" {
cmd.arg("--release");
}
}
status("Building", &format!("{} ({})", target.name, profile));
let out = cmd.status().map_err(|e| format!("cargo: {e}"))?;
if !out.success() {
return Err(format!("cargo build failed for {}", target.name));
}
let artifact = cargo_dir(project, target, profile)
.join(profile)
.join(format!(
"{}{}",
project.manifest.app.name,
std::env::consts::EXE_SUFFIX
));
Ok(BuildOutcome {
target: target.name,
artifact,
seconds: start.elapsed().as_secs_f64(),
})
}
TargetKind::IosSim => crate::mobile::build_ios(project, target, profile, start),
TargetKind::Android => crate::mobile::build_android(project, target, profile, start),
TargetKind::HarmonyOs => crate::ohos::build_ohos(project, target, profile, start),
}
}
const WINUI_MANIFEST: &str = r#"<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<maxversiontested Id="10.0.22621.0"/>
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>
"#;
fn write_winui_manifest(
project: &Project,
target: &Target,
profile: &str,
) -> Result<PathBuf, String> {
let dir = cargo_dir(project, target, profile);
std::fs::create_dir_all(&dir).map_err(|e| format!("manifest dir: {e}"))?;
let path = dir.join("day-winui.manifest");
std::fs::write(&path, WINUI_MANIFEST).map_err(|e| format!("manifest write: {e}"))?;
Ok(path)
}
#[derive(Clone)]
pub struct LaunchSpec {
pub locale: Option<String>,
pub envs: Vec<(String, String)>,
pub attached: bool,
}
pub fn launch(
project: &Project,
target: &'static Target,
outcome: &BuildOutcome,
spec: &LaunchSpec,
) -> Result<std::thread::JoinHandle<i32>, String> {
match target.kind {
TargetKind::Desktop => {
let mut cmd = Command::new(&outcome.artifact);
cmd.current_dir(&project.root)
.env("DAY_ASSET_ROOT", project.root.join("resource/assets"))
.env("DAY_IMAGE_ROOT", project.root.join("resource/images"))
.env("DAY_FONT_ROOT", project.root.join("resource/fonts"));
if spec.attached {
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
} else {
cmd.stdout(Stdio::null()).stderr(Stdio::null());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP);
}
}
if let Some(icon) = crate::resources::app_icon(project, target.toolkit) {
cmd.env("DAY_APP_ICON", &icon);
if target.toolkit == "gtk" && cfg!(target_os = "linux") {
let theme = project.root.join("build/day/gtk/icons");
let apps = theme.join("hicolor/512x512/apps");
let _ = std::fs::create_dir_all(&apps);
let name = &project.manifest.app.id;
if std::fs::copy(&icon, apps.join(format!("{name}.png"))).is_ok() {
cmd.env("DAY_ICON_THEME_DIR", &theme);
cmd.env("DAY_ICON_NAME", name);
}
}
}
if target.toolkit == "gtk" {
cmd.env("GSK_RENDERER", "cairo");
let g = crate::resources::gtk::gresource_path(project);
if g.exists() {
cmd.env("DAY_GRESOURCE", g);
}
}
if target.toolkit == "qt" {
let q = crate::resources::qt::qresource_path(project);
if q.exists() {
cmd.env("DAY_QRESOURCE", q);
}
}
if let Some(locale) = &spec.locale {
cmd.env("DAY_LOCALE", locale);
}
for (k, v) in &spec.envs {
cmd.env(k, v);
}
status("Launching", target.name);
let mut child = cmd.spawn().map_err(|e| format!("spawn: {e}"))?;
crate::signals::register_child(child.id());
let name = target.name;
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let h = std::thread::spawn(move || {
let t1 = stdout.map(|s| stream_logs(name, LogStream::Out, s));
let t2 = stderr.map(|s| stream_logs(name, LogStream::Err, s));
let code = child.wait().map(|s| s.code().unwrap_or(0)).unwrap_or(1);
if let Some(t) = t1 {
let _ = t.join();
}
if let Some(t) = t2 {
let _ = t.join();
}
code
});
Ok(h)
}
TargetKind::IosSim => crate::mobile::launch_ios(project, outcome, spec),
TargetKind::Android => crate::mobile::launch_android(project, outcome, spec),
TargetKind::HarmonyOs => crate::ohos::launch_ohos(project, outcome, spec),
}
}
#[derive(Clone, Copy)]
pub enum LogStream {
Out,
Err,
}
pub fn emit_log(name: &str, stream: LogStream, line: &str) {
match stream {
LogStream::Out => anstream::println!("{LOG_OUT}[{name}]{LOG_OUT:#} {line}"),
LogStream::Err => anstream::eprintln!("{LOG_ERR}[{name}]{LOG_ERR:#} {line}"),
}
}
pub fn stream_logs(
name: &'static str,
stream: LogStream,
src: impl std::io::Read + Send + 'static,
) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
for line in BufReader::new(src).lines().map_while(Result::ok) {
emit_log(name, stream, &line);
}
})
}