pub mod makepad;
use makepad::*;
use core::panic;
use std::{ffi::OsStr, fs, path::{Path, PathBuf}, process::{Command, Stdio}};
const EMPTY_ARGS: std::iter::Empty<&str> = std::iter::empty::<&str>();
const EMPTY_ENVS: std::iter::Empty<(&str, &str)> = std::iter::empty::<(&str, &str)>();
fn resolve_target_dir(path_to_binary: &Path, cwd: &Path) -> PathBuf {
let abs_path = if path_to_binary.is_absolute() {
path_to_binary.to_path_buf()
} else {
cwd.join(path_to_binary)
};
abs_path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| cwd.to_path_buf())
}
fn main() -> std::io::Result<()> {
let mut is_before_packaging = false;
let mut is_before_each_package = false;
let mut main_binary_name = None;
let mut path_to_binary = None;
let mut host_os_opt: Option<String> = None;
let mut args = std::env::args().peekable();
while let Some(arg) = args.next() {
if arg.ends_with("before-packaging") || arg.ends_with("before_packaging") {
is_before_packaging = true;
}
else if arg.contains("before-each") || arg.contains("before_each") {
is_before_each_package = true;
}
else if arg == "--binary-name" {
main_binary_name = Some(args.next().expect("Expected a binary name after '--binary-name'."));
}
else if arg == "--path-to-binary" {
let path = PathBuf::from(args.next().expect("Expected a path after '--path-to-binary'."));
path_to_binary = Some(path);
}
else if arg == "--force-makepad" {
FORCE_MAKEPAD.set(true)
.and(IS_MAKEPAD_APP.set(true))
.expect("only --force-makepad OR --force-no-makepad can be set.");
}
else if arg == "--force-no-makepad" {
FORCE_MAKEPAD.set(false)
.and(IS_MAKEPAD_APP.set(false))
.expect("only --force-makepad OR --force-no-makepad can be set.");
}
else if host_os_opt.is_none() && (arg.contains("host_os") || arg.contains("host-os")) {
host_os_opt = arg
.split("=")
.last()
.map(|s| s.to_string())
.or_else(|| args.peek().map(|s| s.to_string()));
}
}
let main_binary_name = main_binary_name.expect("Missing required argument '--binary-name'");
let path_to_binary = path_to_binary.expect("Missing required argument '--path-to-binary'");
let host_os = host_os_opt.as_deref().unwrap_or(std::env::consts::OS);
match (is_before_packaging, is_before_each_package) {
(true, false) => before_packaging(host_os, &main_binary_name),
(false, true) => before_each_package(host_os, &main_binary_name, &path_to_binary),
(true, true) => panic!("Cannot run both 'before-packaging' and 'before-each-package' commands at the same time."),
(false, false) => panic!("Please specify either the 'before-packaging' or 'before-each-package' command."),
}
}
fn before_packaging(_host_os: &str, _main_binary_name: &str) -> std::io::Result<()> {
Ok(())
}
fn copy_recursively(source: impl AsRef<Path>, destination: impl AsRef<Path>) -> std::io::Result<()> {
fs::create_dir_all(&destination)?;
for entry in fs::read_dir(source)? {
let entry = entry?;
let filetype = entry.file_type()?;
if filetype.is_dir() {
copy_recursively(entry.path(), destination.as_ref().join(entry.file_name()))?;
} else {
fs::copy(entry.path(), destination.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}
fn before_each_package<P: AsRef<Path>>(
host_os: &str,
main_binary_name: &str,
path_to_binary: P,
) -> std::io::Result<()> {
let format = std::env::var("CARGO_PACKAGER_FORMAT")
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
let package_format = format.as_str();
println!("Running before-each-package-command for {package_format:?}");
match package_format {
"app" | "dmg" => before_each_package_macos( package_format, host_os, &main_binary_name, &path_to_binary)?,
"deb" => before_each_package_deb( package_format, host_os, &main_binary_name, &path_to_binary)?,
"appimage" => before_each_package_appimage(package_format, host_os, &main_binary_name, &path_to_binary)?,
"pacman" => before_each_package_pacman( package_format, host_os, &main_binary_name, &path_to_binary)?,
"nsis" => before_each_package_windows( package_format, host_os, &main_binary_name, &path_to_binary)?,
_other => return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Unknown/unsupported package format {_other:?}"),
)),
};
let cwd = std::env::current_dir()?;
let target_dir = resolve_target_dir(path_to_binary.as_ref(), &cwd);
let dist_resources_dir = cwd.join("dist").join("resources");
if dist_resources_dir.exists() {
fs::remove_dir_all(&dist_resources_dir)?;
}
fs::create_dir_all(&dist_resources_dir)?;
{
let app_resources_dest = dist_resources_dir.join(main_binary_name).join("resources");
let app_resources_src = cwd.join("resources");
println!("Copying app-specific resources...\n --> From: {}\n to: {}", app_resources_src.display(), app_resources_dest.display());
copy_recursively(&app_resources_src, &app_resources_dest)?;
}
let should_copy_makepad_resources = match FORCE_MAKEPAD.get() {
Some(forced) => *forced,
None => is_makepad_app(),
};
if should_copy_makepad_resources {
copy_makepad_resources(&dist_resources_dir, &target_dir)?;
}
println!("All resources copied successfully to: {}", dist_resources_dir.display());
println!(" --> Done!");
Ok(())
}
fn before_each_package_macos<P: AsRef<Path>>(
package_format: &str,
host_os: &str,
main_binary_name: &str,
path_to_binary: P,
) -> std::io::Result<()> {
assert!(host_os == "macos", "'app' and 'dmg' packages can only be created on macOS.");
let extra_envs = if is_makepad_app() {
Some(("MAKEPAD", "apple_bundle")).into_iter()
} else {
None.into_iter()
};
cargo_build(
package_format,
host_os,
main_binary_name,
EMPTY_ARGS,
extra_envs,
)?;
let install_name_tool_cmd = Command::new("install_name_tool")
.arg("-add_rpath")
.arg("@executable_path/../Frameworks")
.arg(path_to_binary.as_ref())
.spawn()?;
let output = install_name_tool_cmd.wait_with_output()?;
if !output.status.success() {
eprintln!("Failed to run install_name_tool command: {}
------------------------- stderr: -------------------------
{:?}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Failed to run install_name_tool command for macOS"));
}
Ok(())
}
fn before_each_package_appimage<P: AsRef<Path>>(
package_format: &str,
host_os: &str,
main_binary_name: &str,
path_to_binary: P,
) -> std::io::Result<()> {
assert!(host_os == "linux", "AppImage packages can only be created on Linux.");
cargo_build(
package_format,
host_os,
main_binary_name,
EMPTY_ARGS,
EMPTY_ENVS,
)?;
strip_unneeded_linux_binaries(host_os, path_to_binary)?;
Ok(())
}
fn before_each_package_deb<P: AsRef<Path>>(
package_format: &str,
host_os: &str,
main_binary_name: &str,
path_to_binary: P,
) -> std::io::Result<()> {
assert!(host_os == "linux", "'deb' packages can only be created on Linux.");
cargo_build(
package_format,
host_os,
main_binary_name,
EMPTY_ARGS,
EMPTY_ENVS,
)?;
let ldd_output = Command::new("ldd")
.arg(path_to_binary.as_ref())
.output()?;
let ldd_output = if ldd_output.status.success() {
String::from_utf8_lossy(&ldd_output.stdout)
} else {
eprintln!("Failed to run ldd command: {}
------------------------- stderr: -------------------------
{:?}",
ldd_output.status,
String::from_utf8_lossy(&ldd_output.stderr),
);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to run ldd command on {host_os} for package format {package_format:?}")
));
};
let mut dpkgs = Vec::new();
for line_raw in ldd_output.lines() {
let line = line_raw.trim();
let lib_name_opt = line.split_whitespace()
.next()
.and_then(|path| Path::new(path)
.file_name()
.and_then(|f| f.to_str().to_owned())
);
let Some(lib_name) = lib_name_opt else { continue };
let dpkg_output = Command::new("dpkg")
.arg("-S")
.arg(lib_name)
.stderr(Stdio::null())
.output()?;
let dpkg_output = if dpkg_output.status.success() {
String::from_utf8_lossy(&dpkg_output.stdout)
} else {
continue;
};
let Some(package_name) = dpkg_output.split(':').next() else { continue };
println!("Got dpkg dependency {package_name:?} from ldd output: {line:?}");
dpkgs.push(package_name.to_string());
}
dpkgs.sort();
dpkgs.dedup();
println!("Sorted and de-duplicated dependencies: {:#?}", dpkgs);
std::fs::write("./dist/depends_deb.txt", dpkgs.join("\n"))?;
strip_unneeded_linux_binaries(host_os, path_to_binary)?;
Ok(())
}
fn before_each_package_pacman<P: AsRef<Path>>(
package_format: &str,
host_os: &str,
main_binary_name: &str,
path_to_binary: P,
) -> std::io::Result<()> {
assert!(host_os == "linux", "Pacman packages can only be created on Linux.");
cargo_build(
package_format,
host_os,
main_binary_name,
EMPTY_ARGS,
EMPTY_ENVS,
)?;
strip_unneeded_linux_binaries(host_os, path_to_binary)?;
Ok(())
}
fn before_each_package_windows<P: AsRef<Path>>(
package_format: &str,
host_os: &str,
main_binary_name: &str,
_path_to_binary: P,
) -> std::io::Result<()> {
assert!(host_os == "windows", "'.exe' and '.msi' packages can only be created on Windows.");
cargo_build(
package_format,
host_os,
main_binary_name,
EMPTY_ARGS,
EMPTY_ENVS,
)?;
Ok(())
}
fn cargo_build<I, A, E, K, V>(
package_format: &str,
_host_os: &str,
_main_binary_name: &str,
extra_args: I,
extra_envs: E,
) -> std::io::Result<()>
where
I: IntoIterator<Item = A>,
A: AsRef<OsStr>,
E: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
let mut cargo_build_cmd = Command::new("cargo");
cargo_build_cmd
.arg("build")
.arg("--workspace")
.arg("--release")
.args(extra_args)
.envs(extra_envs);
if is_makepad_app() {
cargo_build_cmd.env(
"MAKEPAD_PACKAGE_DIR",
&makepad_package_dir_value(package_format, _main_binary_name),
);
}
let output = cargo_build_cmd
.spawn()?
.wait_with_output()?;
if !output.status.success() {
eprintln!("Failed to run cargo build command: {}
------------------------- stderr: -------------------------
{:?}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to run cargo build command on {_host_os} for package format {package_format:?}")
));
}
Ok(())
}
fn strip_unneeded_linux_binaries<P: AsRef<Path>>(host_os: &str, path_to_binary: P) -> std::io::Result<()> {
assert!(host_os == "linux", "'strip --strip-unneeded' can only be run on Linux.");
let strip_cmd = Command::new("strip")
.arg("--strip-unneeded")
.arg("--remove-section=.comment")
.arg("--remove-section=.note")
.arg(path_to_binary.as_ref())
.spawn()?;
let output = strip_cmd.wait_with_output()?;
if !output.status.success() {
eprintln!("Failed to run strip command: {}
------------------------- stderr: -------------------------
{:?}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Failed to run strip command for Linux"));
}
Ok(())
}