Files

415 lines
15 KiB
Rust

// We can use `std::process:Command` here because this is invoked within a build script,
// _not_ within the Warp binary (where it could cause a terminal to temporarily flash on
// Windows).
#![allow(clippy::disallowed_types)]
use std::path::Path;
use std::process::Command;
use std::{env, fs};
use anyhow::Result;
use cfg_aliases::cfg_aliases;
use galaxy_util::assets::*;
use galaxy_util::path::app_target_dir;
use sha2::Digest;
use walkdir::WalkDir;
fn main() -> Result<()> {
cfg_aliases! {
linux_or_windows: { any(target_os = "linux", windows) },
enable_crash_recovery: { linux_or_windows },
}
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_OS");
println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_FAMILY");
emit_git_build_metadata();
let target_os = env::var("CARGO_CFG_TARGET_OS")?;
let target_family = env::var("CARGO_CFG_TARGET_FAMILY")?;
add_features(&target_family, &target_os);
if target_os == "macos" && target_family != "wasm" {
println!("cargo:rustc-link-lib=framework=MetalKit");
println!("cargo:rustc-link-lib=framework=UserNotifications");
println!("cargo:rerun-if-changed=src/platform/mac/objc/services.h");
println!("cargo:rerun-if-changed=src/platform/mac/objc/services.m");
cc::Build::new()
.file("src/platform/mac/objc/services.m")
.compile("warp_objc");
// Build the dock tile plugin
println!("cargo:rerun-if-changed=DockTilePlugin/GalaxyDockTilePlugin.m");
println!("cargo:rerun-if-changed=DockTilePlugin/GalaxyDockTilePlugin.h");
println!("cargo:rerun-if-changed=DockTilePlugin/Info.plist");
println!("cargo:rerun-if-changed=DockTilePlugin/Makefile");
let min_macos_version = env::var("MACOSX_DEPLOYMENT_TARGET")
.expect("MACOSX_DEPLOYMENT_TARGET must be set for macos builds");
let status = Command::new("make")
.current_dir("DockTilePlugin")
.env("MACOSX_DEPLOYMENT_TARGET", min_macos_version)
.status()
.expect("Failed to build dock tile plugin");
if !status.success() {
panic!("Dock tile plugin build failed");
}
// Copy the dock tile plugin to the output directory
let profile = get_build_profile_name();
let target_dir = app_target_dir(&profile).expect("Failed to get app target directory");
let plugin_src = Path::new("DockTilePlugin/GalaxyDockTilePlugin.docktileplugin");
let plugin_dst = target_dir.join("GalaxyDockTilePlugin.docktileplugin");
if !status.success() {
fs::remove_dir_all(plugin_src).expect("Failed to clean up plugin directory");
panic!("Dock tile plugin build failed");
}
if plugin_src.exists() {
fs::remove_dir_all(&plugin_dst).ok(); // Remove existing if any
fs::create_dir_all(&plugin_dst).expect("Failed to create plugin directory");
// Copy the plugin directory recursively
for entry in WalkDir::new(plugin_src) {
let entry = entry.expect("Failed to read plugin directory");
let path = entry.path();
let relative = path
.strip_prefix(plugin_src)
.expect("Failed to strip path prefix");
let target = plugin_dst.join(relative);
if path.is_dir() {
fs::create_dir_all(target).expect("Failed to create plugin subdirectory");
} else {
fs::copy(path, target).expect("Failed to copy plugin file");
}
}
// Clean up the source plugin directory after copying
fs::remove_dir_all(plugin_src).expect("Failed to clean up plugin directory");
}
// In standalone mode, embed the Info.plist file. We don't use embed_plist! for this
// because the plist file is dynamically generated.
if env::var("CARGO_FEATURE_STANDALONE").is_ok() {
// Don't fail if INFO_PLIST_PATH is unset, since CI runs clippy with --all-features.
if let Ok(info_plist_path) = env::var("INFO_PLIST_PATH") {
println!("cargo:rerun-if-env-changed=INFO_PLIST_PATH");
println!("cargo:rerun-if-changed={info_plist_path}");
println!("cargo:rustc-link-arg=-sectcreate");
println!("cargo:rustc-link-arg=__TEXT");
println!("cargo:rustc-link-arg=__info_plist");
println!("cargo:rustc-link-arg={info_plist_path}");
} else {
eprintln!("Expected INFO_PLIST_PATH to be set")
}
}
}
if target_os == "windows" {
// Retrieve the Cargo profile name so that we can put a copy of ConPTY in
// the correct target subdirectory.
//
// We need to pass this information manually through an environment variable.
// Of the built-in variables set by Cargo: `OUT_DIR` is only a temporary
// directory, and `PROFILE` can only be `debug` or `release`.
// See https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts
// for more on Cargo environment variables.
//
// Ideally we could access `CARGO_TARGET_DIR` but this doesn't exist at build time.
// See https://github.com/rust-lang/cargo/issues/9661.
//
// Cargo defaults to the `debug` profile.
let cargo_full_profile = env::var("CARGO_FULL_PROFILE").unwrap_or(String::from("debug"));
let target_dir =
app_target_dir(&cargo_full_profile).expect("Could not get app target directory");
copy_windows_assets(&target_dir);
#[cfg(windows)]
embed_resource_file(&target_dir);
}
if target_family == "wasm" {
copy_async_assets();
}
Ok(())
}
fn emit_git_build_metadata() {
const COMMIT_ENV_VARS: [&str; 3] = ["GALAXY_GIT_COMMIT", "GITHUB_SHA", "CI_COMMIT_SHA"];
for variable in COMMIT_ENV_VARS {
println!("cargo:rerun-if-env-changed={variable}");
}
println!("cargo:rerun-if-env-changed=GALAXY_GIT_DIRTY");
// Keep local build metadata current when the checked-out commit changes. Watching the
// package and workspace source directories also refreshes the dirty marker for normal
// development builds without making the repository's `target` directory an input.
println!("cargo:rerun-if-changed=src");
println!("cargo:rerun-if-changed=../crates");
println!("cargo:rerun-if-changed=../resources");
println!("cargo:rerun-if-changed=../Cargo.toml");
println!("cargo:rerun-if-changed=../Cargo.lock");
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_owned());
let repository_root = Path::new(&manifest_dir).parent().unwrap_or(Path::new("."));
if let Some(git_dir) = git_output(repository_root, &["rev-parse", "--absolute-git-dir"]) {
let git_dir = Path::new(&git_dir);
println!("cargo:rerun-if-changed={}", git_dir.join("HEAD").display());
println!("cargo:rerun-if-changed={}", git_dir.join("index").display());
println!(
"cargo:rerun-if-changed={}",
git_dir.join("packed-refs").display()
);
if let Some(head_ref) = git_output(repository_root, &["symbolic-ref", "-q", "HEAD"]) {
println!(
"cargo:rerun-if-changed={}",
git_dir.join(head_ref).display()
);
}
}
let commit = COMMIT_ENV_VARS
.iter()
.find_map(|variable| env::var(variable).ok())
.filter(|value| is_valid_commit(value))
.or_else(|| git_output(repository_root, &["rev-parse", "--verify", "HEAD"]))
.filter(|value| is_valid_commit(value))
.unwrap_or_else(|| "unknown".to_owned());
let dirty = env::var("GALAXY_GIT_DIRTY")
.ok()
.and_then(|value| parse_bool(&value))
.unwrap_or_else(|| {
git_output(
repository_root,
&["status", "--porcelain", "--untracked-files=normal"],
)
.is_some_and(|output| !output.is_empty())
});
println!("cargo:rustc-env=GALAXY_BUILD_GIT_COMMIT={commit}");
println!("cargo:rustc-env=GALAXY_BUILD_GIT_DIRTY={dirty}");
}
fn git_output(repository_root: &Path, arguments: &[&str]) -> Option<String> {
let output = Command::new("git")
.current_dir(repository_root)
.args(arguments)
.output()
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout)
.ok()
.map(|output| output.trim().to_owned())
}
fn is_valid_commit(value: &str) -> bool {
let value = value.trim();
(7..=64).contains(&value.len()) && value.bytes().all(|byte| byte.is_ascii_hexdigit())
}
fn parse_bool(value: &str) -> Option<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" => Some(true),
"0" | "false" | "no" => Some(false),
_ => None,
}
}
fn get_build_profile_name() -> String {
// The profile name is always the 3rd last part of the path (with 1 based indexing).
// e.g. /code/core/target/cli/build/my-build-info-9f91ba6f99d7a061/out
env::var("OUT_DIR")
.expect("OUT_DIR must be set")
.split(std::path::MAIN_SEPARATOR)
.nth_back(3)
.expect("could not get profile name")
.to_string()
}
fn add_features(target_family: &str, target_os: &str) {
if target_family != "wasm" {
println!("cargo:rustc-cfg=feature=\"local_fs\"");
println!("cargo:rustc-cfg=feature=\"local_tty\"");
}
if target_os != "windows" {
println!("cargo:rustc-cfg=feature=\"iterm_images\"");
}
if env::var("PROFILE").ok().is_some_and(|val| val == "debug") {
println!("cargo:rustc-cfg=feature=\"agent_mode_debug\"");
}
}
fn copy_async_assets() {
println!("cargo:rerun-if-changed=assets/async");
println!("cargo:rerun-if-env-changed=ASSET_TARGET_DIR");
let Ok(out_dir_str) = env::var("ASSET_TARGET_DIR") else {
// Don't build assets if no target dir specified.
return;
};
let out_dir = Path::new(&out_dir_str);
let remote_asset_subdirs = &[ASYNC_ASSETS_DIR, REMOTE_ASSETS_DIR];
for remote_asset_subdir in remote_asset_subdirs {
let asset_dir = Path::new(ASSETS_DIR).join(remote_asset_subdir);
for asset in WalkDir::new(&asset_dir) {
let asset = asset.expect("access error");
let asset_path = asset.path();
if asset_path.is_file() {
let contents = fs::read(asset_path).expect("could not read file");
let mut hasher = sha2::Sha256::new();
hasher.update(&contents);
let hash: [u8; 32] = hasher.finalize().into();
let new_relative_path = galaxy_util::assets::hashed_asset_path(
asset_path
.strip_prefix(&asset_dir)
.expect("asset in unexpected location"),
&hash,
);
let new_path = out_dir.join(new_relative_path);
fs::create_dir_all(new_path.parent().unwrap())
.expect("failed to create directories");
fs::write(new_path, contents).expect("failed to copy file");
}
}
}
}
/// Copies the DLLs needed to run Warp on Windows.
///
/// They are organized as follows:
/// - `conpty.dll`
/// - `{platform}/OpenConsole.exe` (ex: `x64/OpenConsole.exe`)
/// - `dxcompiler.dll` (ex: `dxcompiler.dll`)
/// - `dxil.dll` (ex: `dxil.dll`)
fn copy_windows_assets(target_dir: &Path) {
println!("cargo:rerun-if-changed=assets/windows");
let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH")
.expect("Target arhcitecture expected")
.as_str()
{
"x86_64" => "x64",
"aarch64" => "arm64",
_ => {
panic!("Unsupported architecture");
}
};
// This directory is architecture-specific.
let windows_asset_dir = Path::new(ASSETS_DIR)
.join(WINDOWS_ASSETS_DIR)
.join(target_arch);
// Copy conpty.dll into target directory.
fs::copy(
windows_asset_dir.join(CONPTY_DLL_FILE),
target_dir.join(CONPTY_DLL_FILE),
)
.unwrap_or_else(|err| {
panic!("Could not copy conpty.dll from {windows_asset_dir:?} to {target_dir:?}: {err:#}")
});
// Copy the DXC DLLs into the target directory.
for dxc_file in [DXCOMPILER_DLL_FILE, DXIL_DLL_FILE] {
fs::copy(
windows_asset_dir.join(dxc_file),
target_dir.join(dxc_file),
)
.unwrap_or_else(|err| {
panic!("Could not copy {dxc_file} from {windows_asset_dir:?} to {target_dir:?}: {err:#}")
});
}
// Copy OpenConsole.exe into {target_directory}/{arch}.
let old_open_console_exe = windows_asset_dir.join(OPEN_CONSOLE_EXE_FILE);
let new_platform_dir = target_dir.join(target_arch);
let new_open_console_exe = new_platform_dir.join(OPEN_CONSOLE_EXE_FILE);
fs::create_dir_all(&new_platform_dir).expect("Could not create new platform directory");
fs::copy(old_open_console_exe, new_open_console_exe)
.expect("Could not copy platform OpenConsole.exe");
}
#[cfg(windows)]
fn embed_resource_file(target_dir: &Path) {
use std::io::Write;
let version = env::var("GIT_RELEASE_TAG").unwrap_or("v0".to_owned());
let app_name = env::var("GALAXY_APP_NAME").unwrap_or("Galaxy".to_owned());
let icon_path = Path::new("channels/oss/icon/no-padding/icon.ico");
fs::copy(icon_path, target_dir.join("icon.ico"))
.unwrap_or_else(|err| panic!("Could not copy icon: {err:#}"));
let resource_file_path = target_dir.join("resource.rc");
let mut rcfile = fs::File::create(&resource_file_path).unwrap();
write!(
rcfile,
r#"
#pragma code_page(65001)
#include <winres.h>
#define IDI_ICON 0x101
IDI_ICON ICON "icon.ico"
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,0,0
PRODUCTVERSION 1,0,0,0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "CompanyName", "Samsung Electronics of America, Inc."
VALUE "FileDescription", "{app_name}\0"
VALUE "FileVersion", "{version}\0"
VALUE "LegalCopyright", "© 2026, Samsung Electronics Co., Ltd.\0"
VALUE "InternalName", "\0"
VALUE "OriginalFilename", "\0"
VALUE "ProductName", "Galaxy AI\0"
VALUE "ProductVersion", "{version}\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
"#,
)
.unwrap();
drop(rcfile);
// Obtain MSVC environment so that the rc compiler can find the right headers.
// https://github.com/nabijaczleweli/rust-embed-resource/issues/11#issuecomment-603655972
let target = env::var("TARGET").unwrap();
if let Some(tool) = cc::windows_registry::find_tool(target.as_str(), "cl.exe") {
for (key, value) in tool.env() {
env::set_var(key, value);
}
}
embed_resource::compile(resource_file_path, embed_resource::NONE)
.manifest_required()
.unwrap_or_else(|err| panic!("Unable to embed resource file: {err:#}"));
}