ZLUDA/xtask/src/main.rs
2025-06-16 09:53:18 -07:00

328 lines
10 KiB
Rust

use bpaf::{Args, Bpaf, Parser};
use cargo_metadata::{MetadataCommand, Package};
use serde::Deserialize;
use std::{env, ffi::OsString, path::PathBuf, process::Command};
#[derive(Debug, Clone, Bpaf)]
#[bpaf(options)]
enum Options {
#[bpaf(command)]
/// Compile ZLUDA (default command)
Build(#[bpaf(external(build))] Build),
#[bpaf(command)]
/// Compile ZLUDA and build a package
Zip(#[bpaf(external(build))] Build),
}
#[derive(Debug, Clone, Bpaf)]
struct Build {
#[bpaf(any("CARGO", not_help), many)]
/// Arguments to pass to cargo, e.g. `--release` for release build
cargo_arguments: Vec<OsString>,
}
fn not_help(s: OsString) -> Option<OsString> {
if s == "-h" || s == "--help" {
None
} else {
Some(s)
}
}
// We need to sniff out some args passed to cargo to understand how to create
// symlinks (should they go into `target/debug`, `target/release` or custom)
#[derive(Debug, Clone, Bpaf)]
struct Cargo {
#[bpaf(switch, long, short)]
release: Option<bool>,
#[bpaf(long)]
profile: Option<String>,
#[bpaf(any("", Some), many)]
_unused: Vec<OsString>,
}
struct Project {
name: String,
target_name: String,
target_kind: ProjectTarget,
meta: ZludaMetadata,
}
impl Project {
fn try_new(p: Package) -> Option<Project> {
let name = p.name;
serde_json::from_value::<Option<Metadata>>(p.metadata)
.unwrap()
.map(|m| {
let (target_name, target_kind) = p
.targets
.into_iter()
.find_map(|target| {
if target.is_cdylib() {
Some((target.name, ProjectTarget::Cdylib))
} else if target.is_bin() {
Some((target.name, ProjectTarget::Bin))
} else {
None
}
})
.unwrap();
Self {
name,
target_name,
target_kind,
meta: m.zluda,
}
})
}
#[cfg(unix)]
fn prefix(&self) -> &'static str {
match self.target_kind {
ProjectTarget::Bin => "",
ProjectTarget::Cdylib => "lib",
}
}
#[cfg(not(unix))]
fn prefix(&self) -> &'static str {
""
}
#[cfg(unix)]
fn suffix(&self) -> &'static str {
match self.target_kind {
ProjectTarget::Bin => "",
ProjectTarget::Cdylib => ".so",
}
}
#[cfg(not(unix))]
fn suffix(&self) -> &'static str {
match self.target_kind {
ProjectTarget::Bin => ".exe",
ProjectTarget::Cdylib => ".dll",
}
}
// Returns tuple:
// * symlink file path (relative to the root of build dir)
// * symlink absolute file path
// * target actual file (relative to symlink file)
#[cfg_attr(not(unix), allow(unused))]
fn symlinks<'a>(
&'a self,
target_dir: &'a PathBuf,
profile: &'a str,
libname: &'a str,
) -> impl Iterator<Item = (&'a str, PathBuf, PathBuf)> + 'a {
self.meta.linux_symlinks.iter().map(move |source| {
let mut link = target_dir.clone();
link.extend([profile, source]);
let relative_link = PathBuf::from(source);
let ancestors = relative_link.as_path().ancestors().count();
let mut target = std::iter::repeat_with(|| "../").take(ancestors - 2).fold(
PathBuf::new(),
|mut buff, segment| {
buff.push(segment);
buff
},
);
target.push(libname);
(&**source, link, target)
})
}
fn file_name(&self) -> String {
let target_name = &self.target_name;
let prefix = self.prefix();
let suffix = self.suffix();
format!("{prefix}{target_name}{suffix}")
}
}
#[derive(Clone, Copy)]
enum ProjectTarget {
Cdylib,
Bin,
}
#[derive(Deserialize)]
struct Metadata {
zluda: ZludaMetadata,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ZludaMetadata {
#[serde(default)]
windows_only: bool,
#[serde(default)]
debug_only: bool,
#[cfg_attr(not(unix), allow(unused))]
#[serde(default)]
linux_symlinks: Vec<String>,
}
fn main() {
let options = match options().run_inner(Args::current_args()) {
Ok(b) => b,
Err(err) => match build().to_options().run_inner(Args::current_args()) {
Ok(b) => Options::Build(b),
Err(_) => {
err.print_message(100);
std::process::exit(err.exit_code());
}
},
};
match options {
Options::Build(b) => {
compile(b);
}
Options::Zip(b) => zip(b),
}
}
fn compile(b: Build) -> (PathBuf, String, Vec<Project>) {
let profile = sniff_out_profile_name(&b.cargo_arguments);
let meta = MetadataCommand::new().no_deps().exec().unwrap();
let target_directory = meta.target_directory.into_std_path_buf();
let projects = meta
.packages
.into_iter()
.filter_map(Project::try_new)
.filter(|project| {
if project.meta.windows_only && cfg!(not(windows)) {
return false;
}
if project.meta.debug_only && profile != "debug" {
return false;
}
true
})
.collect::<Vec<_>>();
let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let mut command = Command::new(&cargo);
command.arg("build");
command.arg("--locked");
for project in projects.iter() {
command.arg("--package");
command.arg(&project.name);
}
command.args(b.cargo_arguments);
assert!(command.status().unwrap().success());
os::make_symlinks(&target_directory, &*projects, &*profile);
(target_directory, profile, projects)
}
fn sniff_out_profile_name(b: &[OsString]) -> String {
let parsed_cargo_arguments = cargo().to_options().run_inner(b);
match parsed_cargo_arguments {
Ok(Cargo {
release: Some(true),
..
}) => "release".to_string(),
Ok(Cargo {
profile: Some(profile),
..
}) => profile,
_ => "debug".to_string(),
}
}
fn zip(zip: Build) {
let (target_dir, profile, projects) = compile(zip);
os::zip(target_dir, profile, projects)
}
#[cfg(unix)]
mod os {
use flate2::write::GzEncoder;
use flate2::Compression;
use std::{
fs::{self, File},
path::PathBuf,
};
use tar::Header;
pub fn make_symlinks(
target_directory: &std::path::PathBuf,
projects: &[super::Project],
profile: &str,
) {
use std::os::unix::fs as unix_fs;
for project in projects.iter() {
let libname = project.file_name();
for (_, full_path, target) in project.symlinks(target_directory, profile, &libname) {
let mut dir = full_path.clone();
assert!(dir.pop());
fs::create_dir_all(dir).unwrap();
fs::remove_file(&full_path).ok();
unix_fs::symlink(&target, full_path).unwrap();
}
}
}
pub(crate) fn zip(target_dir: PathBuf, profile: String, projects: Vec<crate::Project>) {
let tar_gz =
File::create(format!("{}/{profile}/zluda.tar.gz", target_dir.display())).unwrap();
let enc = GzEncoder::new(tar_gz, Compression::default());
let mut tar = tar::Builder::new(enc);
for project in projects.iter() {
let file_name = project.file_name();
let mut file =
File::open(format!("{}/{profile}/{file_name}", target_dir.display())).unwrap();
tar.append_file(format!("zluda/{file_name}"), &mut file)
.unwrap();
for (source, full_path, target) in project.symlinks(&target_dir, &profile, &file_name) {
let mut header = Header::new_gnu();
let meta = fs::symlink_metadata(&full_path).unwrap();
header.set_metadata(&meta);
tar.append_link(&mut header, format!("zluda/{source}"), target)
.unwrap();
}
}
tar.finish().unwrap();
}
}
#[cfg(not(unix))]
mod os {
use std::{fs::File, io, path::PathBuf};
use zip::{write::SimpleFileOptions, ZipWriter};
pub fn make_symlinks(
_target_directory: &std::path::PathBuf,
_projects: &[super::Project],
_profile: &str,
) {
}
pub(crate) fn zip(target_dir: PathBuf, profile: String, projects: Vec<crate::Project>) {
let zip_file =
File::create(format!("{}/{profile}/zluda.zip", target_dir.display())).unwrap();
let mut zip = ZipWriter::new(zip_file);
zip.add_directory("zluda", SimpleFileOptions::default())
.unwrap();
for project in projects.iter() {
let file_name = project.file_name();
let mut file =
File::open(format!("{}/{profile}/{file_name}", target_dir.display())).unwrap();
let file_options = file_options_from_time(&file).unwrap_or_default();
zip.start_file(format!("zluda/{file_name}"), file_options)
.unwrap();
io::copy(&mut file, &mut zip).unwrap();
}
zip.finish().unwrap();
}
fn file_options_from_time(from: &File) -> io::Result<SimpleFileOptions> {
let metadata = from.metadata()?;
let modified = metadata.modified()?;
let modified = time::OffsetDateTime::from(modified);
Ok(SimpleFileOptions::default().last_modified_time(
zip::DateTime::try_from(modified).map_err(|err| io::Error::other(err))?,
))
}
}