diff --git a/Cargo.toml b/Cargo.toml index 59b94860..a2ab86ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,15 @@ default-run = "cargo-cbuild" [[bin]] name = "cargo-cinstall" -path = "src/main.rs" +path = "src/cinstall.rs" [[bin]] name = "cargo-cbuild" -path = "src/main.rs" +path = "src/cbuild.rs" [dependencies] +cargo = "0.43" +semver = "0.9" log = "0.4" pretty_env_logger = "0.4" cargo_metadata = "0.9" @@ -30,3 +32,4 @@ cbindgen = "0.13.1" toml = "0.5" serde = "1.0" serde_derive = "1.0" +anyhow = "1.0" diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 00000000..2f1fb2f2 --- /dev/null +++ b/src/build.rs @@ -0,0 +1,260 @@ +use std::io::Write; +use std::path::PathBuf; + +use cargo::core::profiles::Profiles; +use cargo::core::{TargetKind, Workspace}; +use cargo::ops; +use cargo::util::command_prelude::{ArgMatches, ArgMatchesExt, CompileMode, ProfileChecking}; +use cargo::{CliResult, Config}; + +use semver::Version; +use structopt::StructOpt; + +use crate::build_targets::BuildTargets; +use crate::install_paths::InstallPaths; +use crate::pkg_config_gen::PkgConfig; +use crate::static_libs::get_static_libs_for_target; +use crate::target; + +/// Build the C header +fn build_include_file( + ws: &Workspace, + name: &str, + version: &Version, + root_output: &PathBuf, + root_path: &PathBuf, +) -> anyhow::Result<()> { + ws.config() + .shell() + .status("Building", "header file using cbindgen")?; + let include_path = root_output.join(&format!("{}.h", name)); + let crate_path = root_path; + + // TODO: map the errors + let mut config = cbindgen::Config::from_root_or_default(crate_path); + let warning = config.autogen_warning.unwrap_or_default(); + let version_info = format!( + "\n#define {0}_MAJOR {1}\n#define {0}_MINOR {2}\n#define {0}_PATCH {3}\n", + name.to_uppercase(), + version.major, + version.minor, + version.patch + ); + config.autogen_warning = Some(warning + &version_info); + cbindgen::Builder::new() + .with_crate(crate_path) + .with_config(config) + .generate() + .unwrap() + .write_to_file(include_path); + + Ok(()) +} + +fn build_pc_file( + ws: &Workspace, + name: &str, + root_output: &PathBuf, + pc: &PkgConfig, +) -> anyhow::Result<()> { + ws.config().shell().status("Building", "pkg-config file")?; + let pc_path = root_output.join(&format!("{}.pc", name)); + + let mut out = std::fs::File::create(pc_path)?; + + let buf = pc.render(); + + out.write_all(buf.as_ref())?; + + Ok(()) +} + +fn patch_lib_kind_in_target(ws: &mut Workspace) -> anyhow::Result<()> { + let pkg = ws.current_mut()?; + let manifest = pkg.manifest_mut(); + let targets = manifest.targets_mut(); + + for target in targets.iter_mut() { + use cargo::core::LibKind::*; + if target.is_lib() { + *target.kind_mut() = + TargetKind::Lib(vec![Lib, Other("staticlib".into()), Other("cdylib".into())]); + } + } + + Ok(()) +} + +/// Build import library for Windows +fn build_implib_file( + ws: &Workspace, + name: &str, + target: &target::Target, + targetdir: &PathBuf, +) -> anyhow::Result<()> { + let os = &target.os; + let env = &target.env; + + if os == "windows" && env == "gnu" { + ws.config() + .shell() + .status("Building", "implib using dlltool")?; + + let arch = &target.arch; + + let binutils_arch = match arch.as_str() { + "x86_64" => "i386:x86-64", + "x86" => "i386", + _ => unimplemented!("Windows support for {} is not implemented yet.", arch), + }; + + let mut dlltool = std::process::Command::new("dlltool"); + dlltool.arg("-m").arg(binutils_arch); + dlltool.arg("-D").arg(format!("{}.dll", name)); + dlltool + .arg("-l") + .arg(targetdir.join(format!("{}.dll.a", name))); + dlltool + .arg("-d") + .arg(targetdir.join(format!("{}.def", name))); + + let out = dlltool.output()?; + if out.status.success() { + Ok(()) + } else { + Err(anyhow::anyhow!("Command failed {:?}", dlltool)) + } + } else { + Ok(()) + } +} + +pub fn cbuild( + ws: &mut Workspace, + config: &Config, + args: &ArgMatches<'_>, +) -> anyhow::Result<(BuildTargets, InstallPaths)> { + let rustc_target = target::Target::new(args.target())?; + let install_paths = InstallPaths::from_matches(args); + + patch_lib_kind_in_target(ws)?; + + let name = ws + .current()? + .manifest() + .targets() + .iter() + .find(|t| t.is_lib()) + .unwrap() + .name(); + let version = ws.current()?.version().clone(); + let root_path = ws.root().to_path_buf(); + + let mut pc = PkgConfig::from_workspace(name, ws, &install_paths); + + let static_libs = get_static_libs_for_target( + rustc_target.verbatim.as_ref(), + &ws.target_dir().as_path_unlocked().to_path_buf(), + )?; + pc.add_lib_private(static_libs); + + let mut compile_opts = args.compile_options( + config, + CompileMode::Build, + Some(ws), + ProfileChecking::Checked, + )?; + + compile_opts.filter = ops::CompileFilter::new( + ops::LibRule::True, + ops::FilterRule::none(), + ops::FilterRule::none(), + ops::FilterRule::none(), + ops::FilterRule::none(), + ); + + compile_opts.export_dir = args.value_of_path("out-dir", config); + if compile_opts.export_dir.is_some() { + config + .cli_unstable() + .fail_if_stable_opt("--out-dir", 6790)?; + } + + let profiles = Profiles::new( + ws.profiles(), + config, + compile_opts.build_config.requested_profile, + ws.features(), + )?; + + // TODO: there must be a simpler way to get the right path. + let root_output = ws + .target_dir() + .as_path_unlocked() + .to_path_buf() + .join(&profiles.get_dir_name()); + + let link_args: Vec = rustc_target + .shared_object_link_args(name, ws, &install_paths.libdir, &root_output) + .into_iter() + .flat_map(|l| vec!["-C".to_string(), format!("link-arg={}", l)]) + .collect(); + + compile_opts.target_rustc_args = Some(link_args); + let r = ops::compile(ws, &compile_opts)?; + assert_eq!(root_output, r.root_output.to_path_buf()); + + build_pc_file(&ws, &name, &root_output, &pc)?; + build_implib_file(&ws, &name, &rustc_target, &root_output)?; + + build_include_file(&ws, &name, &version, &root_output, &root_path)?; + + let build_targets = BuildTargets::new(&name, &rustc_target, &root_output); + + Ok((build_targets, install_paths)) +} + +// TODO: convert to a function using cargo opt() +#[derive(Clone, Debug, StructOpt)] +pub struct Common { + #[structopt(long = "destdir", parse(from_os_str))] + destdir: Option, + #[structopt(long = "prefix", parse(from_os_str))] + prefix: Option, + #[structopt(long = "libdir", parse(from_os_str))] + libdir: Option, + #[structopt(long = "includedir", parse(from_os_str))] + includedir: Option, + #[structopt(long = "bindir", parse(from_os_str))] + bindir: Option, + #[structopt(long = "pkgconfigdir", parse(from_os_str))] + pkgconfigdir: Option, +} + +pub fn config_configure( + config: &mut Config, + args: &ArgMatches<'_>, + subcommand_args: &ArgMatches<'_>, +) -> CliResult { + let arg_target_dir = &subcommand_args.value_of_path("target-dir", config); + let config_args: Vec<&str> = args.values_of("config").unwrap_or_default().collect(); + let quiet = if args.is_present("quiet") || subcommand_args.is_present("quiet") { + Some(true) + } else { + None + }; + config.configure( + args.occurrences_of("verbose") as u32, + quiet, + args.value_of("color"), + args.is_present("frozen"), + args.is_present("locked"), + args.is_present("offline"), + arg_target_dir, + &args + .values_of_lossy("unstable-features") + .unwrap_or_default(), + &config_args, + )?; + Ok(()) +} diff --git a/src/build_targets.rs b/src/build_targets.rs new file mode 100644 index 00000000..8edb04e4 --- /dev/null +++ b/src/build_targets.rs @@ -0,0 +1,53 @@ +use std::path::PathBuf; + +use crate::target::Target; + +#[derive(Debug)] +pub struct BuildTargets { + pub include: PathBuf, + pub static_lib: PathBuf, + pub shared_lib: PathBuf, + pub impl_lib: Option, + pub def: Option, + pub pc: PathBuf, +} + +impl BuildTargets { + pub fn new(name: &str, target: &Target, targetdir: &PathBuf) -> BuildTargets { + let pc = targetdir.join(&format!("{}.pc", name)); + let include = targetdir.join(&format!("{}.h", name)); + + let os = &target.os; + let env = &target.env; + + let (shared_lib, static_lib, impl_lib, def) = match (os.as_str(), env.as_str()) { + ("linux", _) | ("freebsd", _) | ("dragonfly", _) | ("netbsd", _) => { + let static_lib = targetdir.join(&format!("lib{}.a", name)); + let shared_lib = targetdir.join(&format!("lib{}.so", name)); + (shared_lib, static_lib, None, None) + } + ("macos", _) => { + let static_lib = targetdir.join(&format!("lib{}.a", name)); + let shared_lib = targetdir.join(&format!("lib{}.dylib", name)); + (shared_lib, static_lib, None, None) + } + ("windows", "gnu") => { + let static_lib = targetdir.join(&format!("{}.lib", name)); + let shared_lib = targetdir.join(&format!("{}.dll", name)); + let impl_lib = targetdir.join(&format!("{}.dll.a", name)); + let def = targetdir.join(&format!("{}.def", name)); + (shared_lib, static_lib, Some(impl_lib), Some(def)) + } + _ => unimplemented!("The target {}-{} is not supported yet", os, env), + }; + + BuildTargets { + pc, + include, + static_lib, + shared_lib, + impl_lib, + def, + } + } +} diff --git a/src/cbuild.rs b/src/cbuild.rs new file mode 100644 index 00000000..7788b3c8 --- /dev/null +++ b/src/cbuild.rs @@ -0,0 +1,75 @@ +use cargo::util::command_prelude::opt; +use cargo::util::command_prelude::{AppExt, ArgMatchesExt}; +use cargo::CliResult; +use cargo::Config; + +use cargo_c::build::*; + +use structopt::clap::*; +use structopt::StructOpt; + +pub fn cli() -> App<'static, 'static> { + let subcommand = Common::clap() + .name("cbuild") + .arg(opt("quiet", "No output printed to stdout").short("q")) + .arg_package_spec( + "Package to build (see `cargo help pkgid`)", + "Build all packages in the workspace", + "Exclude packages from the build", + ) + .arg_jobs() + .arg_release("Build artifacts in release mode, with optimizations") + .arg_profile("Build artifacts with the specified profile") + .arg_features() + .arg_target_triple("Build for the target triple") + .arg_target_dir() + .arg( + opt( + "out-dir", + "Copy final artifacts to this directory (unstable)", + ) + .value_name("PATH"), + ) + .arg_manifest_path() + .arg_message_format() + .arg_build_plan() + .after_help( + " +Compilation can be configured via the use of profiles which are configured in +the manifest. The default profile for this command is `dev`, but passing +the --release flag will use the `release` profile instead. +", + ); + + app_from_crate!() + .settings(&[ + AppSettings::UnifiedHelpMessage, + AppSettings::DeriveDisplayOrder, + AppSettings::VersionlessSubcommands, + AppSettings::AllowExternalSubcommands, + ]) + .subcommand(subcommand) +} + +fn main() -> CliResult { + let mut config = Config::default()?; + + let args = cli().get_matches(); + + let subcommand_args = match args.subcommand() { + ("cbuild", Some(args)) => args, + _ => { + // No subcommand provided. + cli().print_help()?; + return Ok(()); + } + }; + + config_configure(&mut config, &args, subcommand_args)?; + + let mut ws = subcommand_args.workspace(&config)?; + + let _ = cbuild(&mut ws, &config, &subcommand_args)?; + + Ok(()) +} diff --git a/src/cinstall.rs b/src/cinstall.rs new file mode 100644 index 00000000..290b67c2 --- /dev/null +++ b/src/cinstall.rs @@ -0,0 +1,209 @@ +use std::path::PathBuf; + +use cargo::core::Workspace; +use cargo::util::command_prelude::opt; +use cargo::util::command_prelude::{AppExt, ArgMatchesExt}; +use cargo::CliResult; +use cargo::Config; + +use cargo_c::build::{cbuild, config_configure, Common}; +use cargo_c::build_targets::BuildTargets; +use cargo_c::install_paths::InstallPaths; +use cargo_c::target::Target; + +use structopt::clap::*; +use structopt::StructOpt; + +pub fn cli() -> App<'static, 'static> { + let subcommand = Common::clap() + .name("cinstall") + .arg(opt("quiet", "No output printed to stdout").short("q")) + .arg_package_spec( + "Package to build (see `cargo help pkgid`)", + "Build all packages in the workspace", + "Exclude packages from the build", + ) + .arg_jobs() + .arg_release("Build artifacts in release mode, with optimizations") + .arg_profile("Build artifacts with the specified profile") + .arg_features() + .arg_target_triple("Build for the target triple") + .arg_target_dir() + .arg( + opt( + "out-dir", + "Copy final artifacts to this directory (unstable)", + ) + .value_name("PATH"), + ) + .arg_manifest_path() + .arg_message_format() + .arg_build_plan() + .after_help( + "\ +Compilation can be configured via the use of profiles which are configured in +the manifest. The default profile for this command is `dev`, but passing +the --release flag will use the `release` profile instead. +", + ); + + app_from_crate!() + .settings(&[ + AppSettings::UnifiedHelpMessage, + AppSettings::DeriveDisplayOrder, + AppSettings::VersionlessSubcommands, + ]) + .subcommand(subcommand) +} + +fn append_to_destdir(destdir: &PathBuf, path: &PathBuf) -> PathBuf { + let path = if path.is_absolute() { + let mut components = path.components(); + let _ = components.next(); + components.as_path() + } else { + path.as_path() + }; + + destdir.join(path) +} + +fn cinstall( + ws: &Workspace, + target: &Target, + build_targets: BuildTargets, + paths: InstallPaths, +) -> anyhow::Result<()> { + use std::fs; + + let pkg = ws.current()?; + + let os = &target.os; + let env = &target.env; + let ver = pkg.version(); + + let name = pkg + .manifest() + .targets() + .iter() + .find(|t| t.is_lib()) + .unwrap() + .name(); + + let destdir = &paths.destdir; + + let install_path_lib = append_to_destdir(destdir, &paths.libdir); + let install_path_pc = append_to_destdir(destdir, &paths.pkgconfigdir); + let install_path_include = append_to_destdir(destdir, &paths.includedir).join(name); + let install_path_bin = append_to_destdir(destdir, &paths.bindir); + + fs::create_dir_all(&install_path_lib)?; + fs::create_dir_all(&install_path_pc)?; + fs::create_dir_all(&install_path_include)?; + fs::create_dir_all(&install_path_bin)?; + + ws.config() + .shell() + .status("Installing", "pkg-config file")?; + fs::copy( + &build_targets.pc, + install_path_pc.join(&format!("{}.pc", name)), + )?; + ws.config().shell().status("Installing", "header file")?; + fs::copy( + &build_targets.include, + install_path_include.join(&format!("{}.h", name)), + )?; + ws.config().shell().status("Installing", "static library")?; + fs::copy( + &build_targets.static_lib, + install_path_lib.join(&format!("lib{}.a", name)), + )?; + + ws.config().shell().status("Installing", "shared library")?; + let link_libs = |lib: &str, lib_with_major_ver: &str, lib_with_full_ver: &str| { + let mut ln_sf = std::process::Command::new("ln"); + ln_sf.arg("-sf"); + ln_sf + .arg(lib_with_full_ver) + .arg(install_path_lib.join(lib_with_major_ver)); + let _ = ln_sf.status().unwrap(); + let mut ln_sf = std::process::Command::new("ln"); + ln_sf.arg("-sf"); + ln_sf.arg(lib_with_full_ver).arg(install_path_lib.join(lib)); + let _ = ln_sf.status().unwrap(); + }; + + match (os.as_str(), env.as_str()) { + ("linux", _) | ("freebsd", _) | ("dragonfly", _) | ("netbsd", _) => { + let lib = &format!("lib{}.so", name); + let lib_with_major_ver = &format!("{}.{}", lib, ver.major); + let lib_with_full_ver = &format!("{}.{}.{}", lib_with_major_ver, ver.minor, ver.patch); + fs::copy( + &build_targets.shared_lib, + install_path_lib.join(lib_with_full_ver), + )?; + link_libs(lib, lib_with_major_ver, lib_with_full_ver); + } + ("macos", _) => { + let lib = &format!("lib{}.dylib", name); + let lib_with_major_ver = &format!("lib{}.{}.dylib", name, ver.major); + let lib_with_full_ver = &format!( + "lib{}.{}.{}.{}.dylib", + name, ver.major, ver.minor, ver.patch + ); + fs::copy( + &build_targets.shared_lib, + install_path_lib.join(lib_with_full_ver), + )?; + link_libs(lib, lib_with_major_ver, lib_with_full_ver); + } + ("windows", "gnu") => { + let lib = format!("{}.dll", name); + let impl_lib = format!("lib{}.dll.a", name); + let def = format!("{}.def", name); + fs::copy(&build_targets.shared_lib, install_path_bin.join(lib))?; + fs::copy( + build_targets.impl_lib.as_ref().unwrap(), + install_path_lib.join(impl_lib), + )?; + fs::copy( + build_targets.def.as_ref().unwrap(), + install_path_lib.join(def), + )?; + } + _ => unimplemented!("The target {}-{} is not supported yet", os, env), + } + + Ok(()) +} + +fn main() -> CliResult { + let mut config = Config::default()?; + + let args = cli().get_matches(); + + let subcommand_args = match args.subcommand() { + ("cinstall", Some(args)) => args, + _ => { + // No subcommand provided. + cli().print_help()?; + return Ok(()); + } + }; + + config_configure(&mut config, &args, subcommand_args)?; + + let mut ws = subcommand_args.workspace(&config)?; + + let (build_targets, install_paths) = cbuild(&mut ws, &config, &subcommand_args)?; + + cinstall( + &ws, + &Target::new(args.target())?, + build_targets, + install_paths, + )?; + + Ok(()) +} diff --git a/src/install_paths.rs b/src/install_paths.rs new file mode 100644 index 00000000..f235325c --- /dev/null +++ b/src/install_paths.rs @@ -0,0 +1,50 @@ +use std::path::PathBuf; +use structopt::clap::ArgMatches; + +#[derive(Debug)] +pub struct InstallPaths { + pub destdir: PathBuf, + pub prefix: PathBuf, + pub libdir: PathBuf, + pub includedir: PathBuf, + pub bindir: PathBuf, + pub pkgconfigdir: PathBuf, +} + +impl InstallPaths { + pub fn from_matches(args: &ArgMatches<'_>) -> Self { + let destdir = args + .value_of("destdir") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/")); + let prefix = args + .value_of("prefix") + .map(PathBuf::from) + .unwrap_or_else(|| "/usr/local".into()); + let libdir = args + .value_of("libdir") + .map(PathBuf::from) + .unwrap_or_else(|| prefix.join("lib")); + let includedir = args + .value_of("includedir") + .map(PathBuf::from) + .unwrap_or_else(|| prefix.join("include")); + let bindir = args + .value_of("bindir") + .map(PathBuf::from) + .unwrap_or_else(|| prefix.join("bin")); + let pkgconfigdir = args + .value_of("pkgconfigdir") + .map(PathBuf::from) + .unwrap_or_else(|| libdir.join("pkgconfig")); + + InstallPaths { + destdir, + prefix, + libdir, + includedir, + bindir, + pkgconfigdir, + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..3b007224 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod build; +pub mod build_targets; +pub mod install_paths; +pub mod pkg_config_gen; +pub mod static_libs; +pub mod target; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 9b0fbca3..00000000 --- a/src/main.rs +++ /dev/null @@ -1,683 +0,0 @@ -use std::io::Write; -use std::path::PathBuf; - -use cargo_metadata::{MetadataCommand, Package}; -use log::*; -use structopt::StructOpt; - -mod pkg_config_gen; -mod static_libs; - -use pkg_config_gen::PkgConfig; -use static_libs::get_static_libs_for_target; - -#[derive(Clone, Debug, StructOpt)] -struct Common { - /// Path to the project, by default the current working directory - #[structopt(long = "project-dir", parse(from_os_str))] - projectdir: Option, - /// Number of rustc jobs to run in parallel. - #[structopt(long = "jobs")] - jobs: Option, - /// Build artifacts in release mode, with optimizations - #[structopt(long = "release")] - release: bool, - /// Build for the target triple - #[structopt(long = "target", name = "TRIPLE")] - target: Option, - /// Directory for all generated artifacts - #[structopt(name = "DIRECTORY", long = "target-dir", parse(from_os_str))] - targetdir: Option, - - #[structopt(long = "destdir", parse(from_os_str))] - destdir: Option, - #[structopt(long = "prefix", parse(from_os_str))] - prefix: Option, - #[structopt(long = "libdir", parse(from_os_str))] - libdir: Option, - #[structopt(long = "includedir", parse(from_os_str))] - includedir: Option, - #[structopt(long = "bindir", parse(from_os_str))] - bindir: Option, - #[structopt(long = "pkgconfigdir", parse(from_os_str))] - pkgconfigdir: Option, - - /// Space-separated list of features to activate - #[structopt(long = "features")] - features: Option, - /// Activate all available features - #[structopt(long = "all-features")] - allfeatures: bool, - /// Do not activate the `default` feature - #[structopt(long = "no-default-features")] - nodefaultfeatures: bool, -} - -#[derive(Debug, StructOpt)] -enum Command { - /// Build C-compatible libraries, headers and pkg-config files - #[structopt(name = "build", alias = "cbuild")] - Build { - #[structopt(flatten)] - opts: Common, - }, - - /// Install the C-compatible libraries, headers and pkg-config files - #[structopt(name = "install", alias = "cinstall")] - Install { - #[structopt(flatten)] - opts: Common, - }, -} - -#[derive(Debug, StructOpt)] -struct Opt { - #[structopt(subcommand)] - cmd: Command, -} - -/// Split a target string to its components -/// -/// Because of https://github.com/rust-lang/rust/issues/61558 -/// It uses internally `rustc` to validate the string. -struct Target { - arch: String, - // vendor: String, - os: String, - env: String, - verbatim: Option, -} - -impl Target { - fn new>(target: Option) -> Result { - let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".into()); - let mut cmd = std::process::Command::new(rustc); - - cmd.arg("--print").arg("cfg"); - - if let Some(t) = target.as_ref() { - cmd.arg("--target").arg(t); - } - - let out = cmd.output()?; - if out.status.success() { - fn match_re(re: regex::Regex, s: &str) -> String { - re.captures(s) - .map_or("", |cap| cap.get(1).unwrap().as_str()) - .to_owned() - } - - let arch_re = regex::Regex::new(r#"target_arch="(.+)""#).unwrap(); - // let vendor_re = regex::Regex::new(r#"target_vendor="(.+)""#).unwrap(); - let os_re = regex::Regex::new(r#"target_os="(.+)""#).unwrap(); - let env_re = regex::Regex::new(r#"target_env="(.+)""#).unwrap(); - - let s = std::str::from_utf8(&out.stdout).unwrap(); - - Ok(Target { - arch: match_re(arch_re, s), - // vendor: match_re(vendor_re, s), - os: match_re(os_re, s), - env: match_re(env_re, s), - verbatim: target.map(|v| v.as_ref().to_os_string()), - }) - } else { - Err(std::io::ErrorKind::InvalidInput.into()) - } - } -} - -/// Files we are expected to produce -/// -/// Currently we produce only 1 header, 1 pc file and a variable number of -/// files for the libraries. -#[derive(Debug)] -struct BuildTargets { - include: PathBuf, - static_lib: PathBuf, - shared_lib: PathBuf, - impl_lib: Option, - def: Option, - pc: PathBuf, -} - -impl BuildTargets { - fn new(cfg: &Config, hash: &str) -> BuildTargets { - let name = &cfg.name; - - let pc = cfg.targetdir.join(&format!("{}.pc", name)); - let include = cfg.targetdir.join(&format!("{}.h", name)); - - let os = &cfg.target.os; - let env = &cfg.target.env; - - let targetdir = cfg.targetdir.join("deps"); - - let (shared_lib, static_lib, impl_lib, def) = match (os.as_str(), env.as_str()) { - ("linux", _) | ("freebsd", _) | ("dragonfly", _) | ("netbsd", _) => { - let static_lib = targetdir.join(&format!("lib{}-{}.a", name, hash)); - let shared_lib = targetdir.join(&format!("lib{}-{}.so", name, hash)); - (shared_lib, static_lib, None, None) - } - ("macos", _) => { - let static_lib = targetdir.join(&format!("lib{}-{}.a", name, hash)); - let shared_lib = targetdir.join(&format!("lib{}-{}.dylib", name, hash)); - (shared_lib, static_lib, None, None) - } - ("windows", "gnu") => { - let static_lib = targetdir.join(&format!("{}-{}.lib", name, hash)); - let shared_lib = targetdir.join(&format!("{}-{}.dll", name, hash)); - let impl_lib = cfg.targetdir.join(&format!("{}.dll.a", name)); - let def = cfg.targetdir.join(&format!("{}.def", name)); - (shared_lib, static_lib, Some(impl_lib), Some(def)) - } - _ => unimplemented!("The target {}-{} is not supported yet", os, env), - }; - - BuildTargets { - pc, - include, - static_lib, - shared_lib, - impl_lib, - def, - } - } -} - -use serde_derive::*; - -/// cargo fingerpring of the target crate -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -struct BuildInfo { - hash: String, -} - -/// Configuration required by the command -struct Config { - /// The library name - name: String, - /// Build artifacts in release mode, with optimizations - release: bool, - /// Build for the target triple or the host system. - target: Target, - /// Directory for all generated artifacts with the profile appended. - targetdir: PathBuf, - /// Directory for all generated artifacts without the profile appended. - target_dir: PathBuf, - - destdir: PathBuf, - prefix: PathBuf, - libdir: PathBuf, - includedir: PathBuf, - bindir: PathBuf, - pkgconfigdir: PathBuf, - pkg: Package, - - /// Cargo binary to call - cargo: PathBuf, - - /// Features to pass to the inner call - features: Option, - allfeatures: bool, - nodefaultfeatures: bool, - cli: Common, -} - -fn append_to_destdir(destdir: &PathBuf, path: &PathBuf) -> PathBuf { - let path = if path.is_absolute() { - let mut components = path.components(); - let _ = components.next(); - components.as_path() - } else { - path.as_path() - }; - - destdir.join(path) -} - -impl Config { - fn new(opt: Common) -> Self { - let cli = opt.clone(); - let wd = opt - .projectdir - .unwrap_or_else(|| std::env::current_dir().unwrap()); - - let mut cmd = MetadataCommand::new(); - - cmd.current_dir(&wd); - cmd.manifest_path(wd.join("Cargo.toml")); - - let meta = cmd.exec().unwrap(); - - let pkg = meta - .packages - .iter() - .find(|p| p.id.repr == meta.workspace_members.first().unwrap().repr) - .unwrap(); - - let target_dir = opt.targetdir.as_ref().unwrap_or(&meta.target_directory); - let profile = if opt.release { "release" } else { "debug" }; - let targetdir = target_dir.join(profile); - - let prefix = opt.prefix.unwrap_or_else(|| "/usr/local".into()); - let libdir = opt.libdir.unwrap_or_else(|| prefix.join("lib")); - let includedir = opt.includedir.unwrap_or_else(|| prefix.join("include")); - let bindir = opt.bindir.unwrap_or_else(|| prefix.join("bin")); - let pkgconfigdir = opt.pkgconfigdir.unwrap_or_else(|| libdir.join("pkgconfig")); - - let name = pkg - .targets - .iter() - .find(|t| t.kind.iter().any(|x| x == "lib")) - .expect("Cannot find a library target") - .name - .replace("-", "_"); - - Config { - name, - release: opt.release, - target: Target::new(opt.target.as_ref()).unwrap(), - destdir: opt.destdir.unwrap_or_else(|| PathBuf::from("/")), - - targetdir, - target_dir: target_dir.clone(), - prefix, - libdir, - includedir, - bindir, - pkgconfigdir, - pkg: pkg.clone(), - cargo: std::env::var("CARGO") - .unwrap_or_else(|_| "cargo".into()) - .into(), - features: opt.features, - allfeatures: opt.allfeatures, - nodefaultfeatures: opt.nodefaultfeatures, - cli, - } - } - - fn open_build_info(&self) -> Option { - let info_path = self.targetdir.join(".cargo-c.toml"); - let mut f = std::fs::File::open(info_path).ok()?; - - use std::io::Read; - let mut s = Vec::new(); - - f.read_to_end(&mut s).ok()?; - - let t = toml::from_slice::(&s).unwrap(); - - info!("saved build hash {}", t.hash); - - Some(t) - } - - fn save_build_info(&self, info: &BuildInfo) { - let info_path = self.targetdir.join(".cargo-c.toml"); - let mut f = std::fs::File::create(info_path).unwrap(); - let s = toml::to_vec(info).unwrap(); - - f.write_all(&s).unwrap(); - } - - /// Build the pkg-config file - fn build_pc_file(&self, build_targets: &BuildTargets) -> Result<(), std::io::Error> { - log::info!("Building PkgConfig for {}", build_targets.pc.display()); - let mut pc = PkgConfig::from_config(&self); - let target_dir = &self.targetdir; - let static_libs = get_static_libs_for_target(self.target.verbatim.as_ref(), target_dir)?; - - pc.add_lib_private(static_libs); - - let pc_path = &build_targets.pc; - let mut out = std::fs::File::create(pc_path)?; - - let buf = pc.render(); - - out.write_all(buf.as_ref())?; - - Ok(()) - } - - /// Build import library for Windows - fn build_implib_file(&self) -> Result<(), std::io::Error> { - log::info!( - "Building implib using dlltool for target {}", - self.target - .verbatim - .as_ref() - .map(|os| os.to_string_lossy().into_owned()) - .unwrap_or("native".into()) - ); - let os = &self.target.os; - let env = &self.target.env; - - if os == "windows" && env == "gnu" { - let name = &self.name; - let arch = &self.target.arch; - let target_dir = &self.targetdir; - - let binutils_arch = match arch.as_str() { - "x86_64" => "i386:x86-64", - "x86" => "i386", - _ => unimplemented!("Windows support for {} is not implemented yet.", arch), - }; - - let mut dlltool = std::process::Command::new("dlltool"); - dlltool.arg("-m").arg(binutils_arch); - dlltool.arg("-D").arg(format!("{}.dll", name)); - dlltool - .arg("-l") - .arg(target_dir.join(format!("{}.dll.a", name))); - dlltool - .arg("-d") - .arg(target_dir.join(format!("{}.def", name))); - - let out = dlltool.output()?; - if out.status.success() { - Ok(()) - } else { - Err(std::io::ErrorKind::InvalidInput.into()) - } - } else { - Ok(()) - } - } - - /// Build the C header - fn build_include_file(&self, build_targets: &BuildTargets) -> Result<(), std::io::Error> { - log::info!("Building include file using cbindgen"); - let include_path = &build_targets.include; - let crate_path = self.pkg.manifest_path.parent().unwrap(); - - // TODO: map the errors - let mut config = cbindgen::Config::from_root_or_default(crate_path); - let warning = config.autogen_warning.unwrap_or_default(); - let version_info = format!( - "\n#define {0}_MAJOR {1}\n#define {0}_MINOR {2}\n#define {0}_PATCH {3}\n", - self.name.to_uppercase(), - self.pkg.version.major, - self.pkg.version.minor, - self.pkg.version.patch - ); - config.autogen_warning = Some(warning + &version_info); - cbindgen::Builder::new() - .with_crate(crate_path) - .with_config(config) - .generate() - .unwrap() - .write_to_file(include_path); - - Ok(()) - } - - /// Return a list of linker arguments useful to produce a platform-correct dynamic library - fn shared_object_link_args(&self) -> Vec { - let mut lines = Vec::new(); - let name = &self.name; - - let major = self.pkg.version.major; - let minor = self.pkg.version.minor; - let patch = self.pkg.version.patch; - - let os = &self.target.os; - let env = &self.target.env; - - let libdir = &self.libdir; - let target_dir = &self.targetdir; - - if os == "linux" || os == "freebsd" || os == "dragonfly" || os == "netbsd" { - lines.push(format!("-Wl,-soname,lib{}.so.{}", name, major)); - } else if os == "macos" { - let line = format!("-Wl,-install_name,{1}/lib{0}.{2}.{3}.{4}.dylib,-current_version,{2}.{3}.{4},-compatibility_version,{2}", - name, libdir.display(), major, minor, patch); - lines.push(line) - } else if os == "windows" && env == "gnu" { - // This is only set up to work on GNU toolchain versions of Rust - lines.push(format!( - "-Wl,--output-def,{}", - target_dir.join(format!("{}.def", name)).display() - )); - } - - lines - } - - /// Build the Library - fn build_library(&self) -> Result, std::io::Error> { - log::info!("Building the libraries using cargo rustc"); - use std::io; - let mut cmd = std::process::Command::new(&self.cargo); - - cmd.arg("rustc"); - cmd.arg("-v"); - cmd.arg("--lib"); - cmd.arg("--target-dir").arg(&self.target_dir); - cmd.arg("--manifest-path").arg(&self.pkg.manifest_path); - - if let Some(jobs) = self.cli.jobs { - cmd.arg("--jobs").arg(jobs.to_string()); - } - - if let Some(t) = self.target.verbatim.as_ref() { - cmd.arg("--target").arg(t); - } - - if self.release { - cmd.arg("--release"); - } - - if let Some(features) = self.features.as_ref() { - cmd.arg("--features").arg(features); - } - - if self.allfeatures { - cmd.arg("--all-features"); - } - - if self.nodefaultfeatures { - cmd.arg("--no-default-features"); - } - - cmd.arg("--"); - - cmd.arg("--crate-type").arg("staticlib"); - cmd.arg("--crate-type").arg("cdylib"); - - cmd.arg("--cfg").arg("cargo_c"); - - for line in self.shared_object_link_args() { - cmd.arg("-C").arg(&format!("link-arg={}", line)); - } - info!("build_library {:?}", cmd); - - let out = cmd.output()?; - - io::stdout().write_all(&out.stdout).unwrap(); - io::stderr().write_all(&out.stderr).unwrap(); - - if !out.status.success() { - return Err(std::io::ErrorKind::InvalidInput.into()); - } - // TODO: replace this hack with something saner - let exp = &format!(".* -C extra-filename=-([^ ]*) .*"); - // println!("exp : {}", exp); - let re = regex::Regex::new(exp).unwrap(); - let s = std::str::from_utf8(&out.stderr).unwrap(); - - let fresh_line = format!("Fresh {} ", self.pkg.name); - - let is_fresh = s.lines().rfind(|line| line.contains(&fresh_line)).is_some(); - - if !is_fresh { - let s = s - .lines() - .rfind(|line| line.contains("--cfg cargo_c")) - .unwrap(); - - let hash = re - .captures(s) - .map(|cap| cap.get(1).unwrap().as_str()) - .unwrap() - .to_owned(); - - info!("parsed hash {}", hash); - - Ok(Some(BuildInfo { hash })) - } else { - Ok(None) - } - } - - fn build(&self) -> Result { - log::info!("Building"); - std::fs::create_dir_all(&self.targetdir)?; - - let prev_info = self.open_build_info(); - - let mut info = self.build_library()?; - - if info.is_none() && prev_info.is_none() { - let mut cmd = std::process::Command::new(&self.cargo); - cmd.arg("clean"); - - cmd.status()?; - info = Some(self.build_library()?.unwrap()); - } - - let info = if prev_info.is_none() || (info.is_some() && info != prev_info) { - let info = info.unwrap(); - let build_targets = BuildTargets::new(self, &info.hash); - - self.build_pc_file(&build_targets)?; - self.build_implib_file()?; - self.build_include_file(&build_targets)?; - - self.save_build_info(&info); - info - } else { - eprintln!("Already built"); - prev_info.unwrap() - }; - - Ok(info) - } - - fn install(&self, build_targets: BuildTargets) -> Result<(), std::io::Error> { - log::info!("Installing"); - use std::fs; - // TODO make sure the build targets exist and are up to date - // println!("{:?}", self.build_targets); - - let os = &self.target.os; - let env = &self.target.env; - let name = &self.name; - let ver = &self.pkg.version; - - let install_path_lib = append_to_destdir(&self.destdir, &self.libdir); - let install_path_pc = append_to_destdir(&self.destdir, &self.pkgconfigdir); - let install_path_include = append_to_destdir(&self.destdir, &self.includedir).join(name); - let install_path_bin = append_to_destdir(&self.destdir, &self.bindir); - - // fs::create_dir_all(&install_path_lib); - fs::create_dir_all(&install_path_pc)?; - fs::create_dir_all(&install_path_include)?; - fs::create_dir_all(&install_path_bin)?; - - fs::copy( - &build_targets.pc, - install_path_pc.join(&format!("{}.pc", name)), - )?; - fs::copy( - &build_targets.include, - install_path_include.join(&format!("{}.h", name)), - )?; - fs::copy( - &build_targets.static_lib, - install_path_lib.join(&format!("lib{}.a", name)), - )?; - - let link_libs = |lib: &str, lib_with_major_ver: &str, lib_with_full_ver: &str| { - let mut ln_sf = std::process::Command::new("ln"); - ln_sf.arg("-sf"); - ln_sf - .arg(lib_with_full_ver) - .arg(install_path_lib.join(lib_with_major_ver)); - let _ = ln_sf.status().unwrap(); - let mut ln_sf = std::process::Command::new("ln"); - ln_sf.arg("-sf"); - ln_sf.arg(lib_with_full_ver).arg(install_path_lib.join(lib)); - let _ = ln_sf.status().unwrap(); - }; - - match (os.as_str(), env.as_str()) { - ("linux", _) | ("freebsd", _) | ("dragonfly", _) | ("netbsd", _) => { - let lib = &format!("lib{}.so", name); - let lib_with_major_ver = &format!("{}.{}", lib, ver.major); - let lib_with_full_ver = - &format!("{}.{}.{}", lib_with_major_ver, ver.minor, ver.patch); - fs::copy( - &build_targets.shared_lib, - install_path_lib.join(lib_with_full_ver), - )?; - link_libs(lib, lib_with_major_ver, lib_with_full_ver); - } - ("macos", _) => { - let lib = &format!("lib{}.dylib", name); - let lib_with_major_ver = &format!("lib{}.{}.dylib", name, ver.major); - let lib_with_full_ver = &format!( - "lib{}.{}.{}.{}.dylib", - name, ver.major, ver.minor, ver.patch - ); - fs::copy( - &build_targets.shared_lib, - install_path_lib.join(lib_with_full_ver), - )?; - link_libs(lib, lib_with_major_ver, lib_with_full_ver); - } - ("windows", "gnu") => { - let lib = format!("{}.dll", name); - let impl_lib = format!("lib{}.dll.a", name); - let def = format!("{}.def", name); - fs::copy(&build_targets.shared_lib, install_path_bin.join(lib))?; - fs::copy( - build_targets.impl_lib.as_ref().unwrap(), - install_path_lib.join(impl_lib), - )?; - fs::copy( - build_targets.def.as_ref().unwrap(), - install_path_lib.join(def), - )?; - } - _ => unimplemented!("The target {}-{} is not supported yet", os, env), - } - - Ok(()) - } -} - -fn main() -> Result<(), std::io::Error> { - pretty_env_logger::init(); - let opts = Opt::from_args(); - - match opts.cmd { - Command::Build { opts } => { - let cfg = Config::new(opts); - cfg.build()?; - } - Command::Install { opts } => { - let cfg = Config::new(opts); - - let info = cfg.build()?; - let build_targets = BuildTargets::new(&cfg, &info.hash); - - info!("{:?}", build_targets); - - cfg.install(build_targets)?; - } - } - - Ok(()) -} diff --git a/src/pkg_config_gen.rs b/src/pkg_config_gen.rs index ba6e9525..737bdd27 100644 --- a/src/pkg_config_gen.rs +++ b/src/pkg_config_gen.rs @@ -1,9 +1,8 @@ #![allow(dead_code)] +use crate::install_paths::InstallPaths; use std::path::PathBuf; -use crate::Config; - #[derive(Debug, Clone)] pub struct PkgConfig { prefix: PathBuf, @@ -60,7 +59,7 @@ impl PkgConfig { includedir: "${prefix}/include".into(), libdir: "${exec_prefix}/lib".into(), - libs: vec![format!("-L{} -l{}", "${libdir}", name).to_owned()], + libs: vec![format!("-L{} -l{}", "${libdir}", name)], libs_private: Vec::new(), requires: Vec::new(), @@ -72,23 +71,25 @@ impl PkgConfig { } } - pub(crate) fn from_config(cfg: &Config) -> Self { - let name = &cfg.name; - let version = cfg.pkg.version.to_string(); + pub(crate) fn from_workspace( + name: &str, + ws: &cargo::core::Workspace, + install_paths: &InstallPaths, + ) -> Self { + let pkg = ws.current().unwrap(); + let version = pkg.version().to_string(); + let description = &pkg.manifest().metadata().description; + let mut pc = PkgConfig::new(name, version); - if cfg.pkg.description.is_some() { - pc.description = cfg.pkg.description.as_ref().unwrap().to_owned(); + if let Some(ref d) = description { + pc.description = d.clone(); } - pc.prefix = cfg.prefix.clone(); + pc.prefix = install_paths.prefix.clone(); // TODO: support exec_prefix - if cfg.cli.includedir.is_some() { - pc.includedir = cfg.includedir.clone(); - } - if cfg.cli.libdir.is_some() { - pc.libdir = cfg.libdir.clone(); - } + pc.includedir = install_paths.includedir.clone(); + pc.libdir = install_paths.libdir.clone(); pc } @@ -157,8 +158,7 @@ Cflags: {}", self.version, self.libs.join(" "), self.cflags.join(" "), - ) - .to_owned(); + ); if !self.libs_private.is_empty() { base.push_str(&format!( diff --git a/src/static_libs.rs b/src/static_libs.rs index 83362834..b8788813 100644 --- a/src/static_libs.rs +++ b/src/static_libs.rs @@ -1,9 +1,9 @@ use std::ffi::OsString; -use std::io::Result; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::{env, str}; +use anyhow::Result; use regex::Regex; pub fn get_static_libs_for_target>( @@ -41,6 +41,6 @@ pub fn get_static_libs_for_target>( .map_or("", |cap| cap.get(1).unwrap().as_str()) .to_owned()) } else { - Err(std::io::ErrorKind::InvalidData.into()) + Err(anyhow::anyhow!("cannot run {:?}", cmd)) } } diff --git a/src/target.rs b/src/target.rs new file mode 100644 index 00000000..96e277a8 --- /dev/null +++ b/src/target.rs @@ -0,0 +1,93 @@ +use std::path::PathBuf; + +use anyhow::*; +use cargo::core::Workspace; + +/// Split a target string to its components +/// +/// Because of https://github.com/rust-lang/rust/issues/61558 +/// It uses internally `rustc` to validate the string. +#[derive(Debug)] +pub struct Target { + pub arch: String, + // pub vendor: String, + pub os: String, + pub env: String, + pub verbatim: Option, +} + +impl Target { + pub fn new>(target: Option) -> Result { + let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".into()); + let mut cmd = std::process::Command::new(rustc); + + cmd.arg("--print").arg("cfg"); + + if let Some(t) = target.as_ref() { + cmd.arg("--target").arg(t); + } + + let out = cmd.output()?; + if out.status.success() { + fn match_re(re: regex::Regex, s: &str) -> String { + re.captures(s) + .map_or("", |cap| cap.get(1).unwrap().as_str()) + .to_owned() + } + + let arch_re = regex::Regex::new(r#"target_arch="(.+)""#).unwrap(); + // let vendor_re = regex::Regex::new(r#"target_vendor="(.+)""#).unwrap(); + let os_re = regex::Regex::new(r#"target_os="(.+)""#).unwrap(); + let env_re = regex::Regex::new(r#"target_env="(.+)""#).unwrap(); + + let s = std::str::from_utf8(&out.stdout).unwrap(); + + Ok(Target { + arch: match_re(arch_re, s), + // vendor: match_re(vendor_re, s), + os: match_re(os_re, s), + env: match_re(env_re, s), + verbatim: target.map(|v| v.as_ref().to_os_string()), + }) + } else { + Err(anyhow!("Cannot run {:?}", cmd)) + } + } + + /// Build a list of linker arguments + pub fn shared_object_link_args( + &self, + name: &str, + ws: &Workspace, + libdir: &PathBuf, + target_dir: &PathBuf, + ) -> Vec { + let mut lines = Vec::new(); + + let pkg = ws.current().unwrap(); + let version = pkg.version(); + + let major = version.major; + let minor = version.minor; + let patch = version.patch; + + let os = &self.os; + let env = &self.env; + + if os == "linux" || os == "freebsd" || os == "dragonfly" || os == "netbsd" { + lines.push(format!("-Wl,-soname,lib{}.so.{}", name, major)); + } else if os == "macos" { + let line = format!("-Wl,-install_name,{1}/lib{0}.{2}.{3}.{4}.dylib,-current_version,{2}.{3}.{4},-compatibility_version,{2}", + name, libdir.display(), major, minor, patch); + lines.push(line) + } else if os == "windows" && env == "gnu" { + // This is only set up to work on GNU toolchain versions of Rust + lines.push(format!( + "-Wl,--output-def,{}", + target_dir.join(format!("{}.def", name)).display() + )); + } + + lines + } +}