Skip to content

Commit

Permalink
WIP: Add support for cross-compilation
Browse files Browse the repository at this point in the history
This fixes #524.

Changelog: added
  • Loading branch information
yorickpeterse committed Jan 10, 2024
1 parent a69b423 commit ed180e5
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 77 deletions.
54 changes: 49 additions & 5 deletions compiler/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ const MAIN_TEST_MODULE: &str = "inko-tests";
/// The name of the directory to store build files in.
const BUILD: &str = "build";

fn home_dir() -> Option<PathBuf> {
env::var_os("HOME").filter(|v| !v.is_empty()).map(PathBuf::from)
}

pub fn data_directory() -> Option<PathBuf> {
let base = if cfg!(target_os = "macos") {
home_dir().map(|h| h.join("Library").join("Application Support"))
} else {
env::var_os("XDG_DATA_HOME")
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.or_else(|| home_dir().map(|h| h.join(".local").join("share")))
};

base.map(|p| p.join("inko"))
}

pub(crate) fn local_runtimes_directory() -> Option<PathBuf> {
// The Inko ABI isn't stable, so runtimes are scoped to the Inko version
// they were compiled for.
data_directory().map(|p| p.join("runtimes").join(env!("CARGO_PKG_VERSION")))
}

fn create_directory(path: &Path) -> Result<(), String> {
if path.is_dir() {
return Ok(());
Expand Down Expand Up @@ -134,7 +157,7 @@ pub enum Output {
}

/// A type describing which linker to use.
#[derive(Copy, Clone)]
#[derive(Clone)]
pub enum Linker {
/// Detect which linker to use.
Detect,
Expand All @@ -147,6 +170,12 @@ pub enum Linker {

/// Always use Mold.
Mold,

/// Always use Zig.
Zig,

/// Use a custom linker with any extra arguments.
Custom(String),
}

impl Linker {
Expand All @@ -155,9 +184,15 @@ impl Linker {
"system" => Some(Linker::System),
"lld" => Some(Linker::Lld),
"mold" => Some(Linker::Mold),
"zig" => Some(Linker::Zig),
_ if !value.is_empty() => Some(Linker::Custom(value.to_string())),
_ => None,
}
}

pub(crate) fn is_zig(&self) -> bool {
matches!(self, Linker::Zig)
}
}

/// A type for storing compiler configuration, such as the source directories to
Expand All @@ -166,9 +201,8 @@ pub struct Config {
/// The directory containing the Inko's standard library.
pub(crate) std: PathBuf,

/// The directory containing runtime library files to link to the generated
/// code.
pub runtime: PathBuf,
/// The directories to search for Inko's runtime library.
pub runtimes: Vec<PathBuf>,

/// The directory containing the project's source code.
pub(crate) source: PathBuf,
Expand Down Expand Up @@ -219,6 +253,9 @@ pub struct Config {
/// The linker to use.
pub linker: Linker,

/// Extra arguments to pass to the linker.
pub linker_arguments: Vec<String>,

/// If incremental compilation is enabled or not.
pub incremental: bool,

Expand All @@ -240,9 +277,15 @@ impl Config {
.and_then(|m| m.modified())
.unwrap_or_else(|_| SystemTime::now());

let mut runtimes = vec![PathBuf::from(env!("INKO_RT"))];

if let Some(dir) = local_runtimes_directory() {
runtimes.push(dir);
}

Self {
std,
runtime: PathBuf::from(env!("INKO_RT")),
runtimes,
source: cwd.join(SOURCE),
tests: cwd.join(TESTS),
build: cwd.join(BUILD),
Expand All @@ -259,6 +302,7 @@ impl Config {
static_linking: false,
threads: available_parallelism().map(|v| v.get()).unwrap_or(1),
linker: Linker::Detect,
linker_arguments: Vec::new(),
incremental: true,
compiled_at,
}
Expand Down
168 changes: 126 additions & 42 deletions compiler/src/linker.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::config::{Config, Linker};
use crate::state::State;
use crate::target::OperatingSystem;
use std::io::Read as _;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

Expand All @@ -14,19 +15,21 @@ fn runtime_library(config: &Config) -> Option<PathBuf> {
files.push("libinko.a".to_string());
}

files.iter().find_map(|file| {
let path = config.runtime.join(file);
for rt in &config.runtimes {
for file in &files {
let path = rt.join(file);

if path.is_file() {
Some(path)
} else {
None
if path.is_file() {
return Some(path);
}
}
})
}

None
}

fn linker_is_available(linker: &str) -> bool {
Command::new(linker)
fn command_is_available(name: &str) -> bool {
Command::new(name)
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
Expand All @@ -36,27 +39,128 @@ fn linker_is_available(linker: &str) -> bool {
.map_or(false, |status| status.success())
}

fn cc_is_clang() -> bool {
let Ok(mut child) = Command::new("cc")
.arg("--version")
.stdout(Stdio::piped())
.stderr(Stdio::null())
.stdin(Stdio::null())
.spawn()
else {
return false;
};

let mut stdout = child.stdout.take().unwrap();
let Ok(status) = child.wait() else { return false };
let mut output = String::new();
let _ = stdout.read_to_string(&mut output);

status.success() && output.contains("clang version")
}

fn lld_is_available() -> bool {
linker_is_available("ld.lld")
command_is_available("ld.lld")
}

fn mold_is_available() -> bool {
linker_is_available("ld.mold")
command_is_available("ld.mold")
}

fn driver(state: &State) -> Result<Command, String> {
let triple = state.config.target.llvm_triple();
let cmd = if let Linker::Custom(name) = &state.config.linker {
Command::new(name)
} else if state.config.linker.is_zig() {
let mut cmd = Command::new("zig");

cmd.arg("cc");

// To make using Zig as a linker a bit easier, we translate our target
// triples into those used by Zig (which in turn are a bit different
// from the ones used by LLVM).
cmd.arg(&format!("--target={}", state.config.target.zig_triple()));
cmd
} else {
let gcc_exe = format!("{}-gcc", triple);
let mut linker = state.config.linker.clone();
let mut cmd = if state.config.target.is_native() {
Command::new("cc")
} else if command_is_available(&gcc_exe) {
// GCC cross compilers don't support the `-fuse-ld` flag, so in this
// case we force the use of the system linker.
linker = Linker::System;
Command::new(gcc_exe)
} else if cc_is_clang() || command_is_available("clang") {
// We check for clang _after_ GCC, because if a dedicated GCC
// executable for the target is available, using it is less prone to
// error as we don't have to bother finding the right sysroot.
Command::new("clang")
} else {
return Err(format!(
"you are cross-compiling to {}, but the linker used (cc) \
doesn't support cross-compilation. You can specify a custom \
linker using the --linker=LINKER option",
triple
));
};

if cmd.get_program() == "clang" {
// clang tends to pick the host version of any necessary libraries
// (including crt1.o and the likes) when cross-compiling. We try to
// fix this here by automatically setting the correct sysroot.
//
// Linux distributions (Arch Linux, Fedora, Ubuntu, etc) typically
// install the toolchains in /usr, e.g. /usr/aarch64-linux-gnu.
//
// For other platforms we don't bother trying to find the sysroot,
// as they don't reside in a consistent location.
if cfg!(target_os = "linux") {
let path = format!("/usr/{}", triple);

if Path::new(&path).is_dir() {
cmd.arg(format!("--sysroot={}", path));
}
}

cmd.arg(&format!("--target={}", triple));
}

if let Linker::Detect = linker {
// Mold doesn't support macOS, so we don't enable it for macOS
// targets.
if mold_is_available() && !state.config.target.os.is_mac() {
linker = Linker::Mold;
} else if lld_is_available() {
linker = Linker::Lld;
}
}

match linker {
Linker::Lld => {
cmd.arg("-fuse-ld=lld");
}
Linker::Mold => {
cmd.arg("-fuse-ld=mold");
}
_ => {}
}

cmd
};

Ok(cmd)
}

pub(crate) fn link(
state: &State,
output: &Path,
paths: &[PathBuf],
) -> Result<(), String> {
// On Unix systems the necessary libraries/object files are all over the
// place. Instead of re-implementing the logic necessary to find these
// files, we rely on the system's compiler to do this for us.
//
// As we only use this executable for linking it doesn't really matter
// if this ends up using gcc, clang or something else, because we only
// use it as a wrapper around the linker executable.
let mut cmd = Command::new("cc");
let mut cmd = driver(state)?;

for arg in &state.config.linker_arguments {
cmd.arg(arg);
}

// Object files must come before any of the libraries to link against, as
// certain linkers are very particular about the order of flags such as
Expand Down Expand Up @@ -87,7 +191,7 @@ pub(crate) fn link(
OperatingSystem::Linux => {
// Certain versions of Linux (e.g. Debian 11) also need libdl and
// libpthread to be linked in explicitly. We use the --as-needed
// flag here (supported by both gcc and clang) to only link these
// flag here (supported by both GCC and clang) to only link these
// libraries if actually needed.
cmd.arg("-Wl,--as-needed");
cmd.arg("-ldl");
Expand Down Expand Up @@ -146,26 +250,6 @@ pub(crate) fn link(
cmd.arg("-static-libgcc");
}

let mut linker = state.config.linker;

if let Linker::Detect = linker {
if mold_is_available() {
linker = Linker::Mold;
} else if lld_is_available() {
linker = Linker::Lld;
}
}

match linker {
Linker::Lld => {
cmd.arg("-fuse-ld=lld");
}
Linker::Mold => {
cmd.arg("-fuse-ld=mold");
}
_ => {}
}

cmd.stdin(Stdio::null());
cmd.stderr(Stdio::piped());
cmd.stdout(Stdio::null());
Expand All @@ -182,9 +266,9 @@ pub(crate) fn link(
Ok(())
} else {
Err(format!(
"The linker exited with status code {}:\n{}",
"the linker exited with status code {}:\n\n{}",
output.status.code().unwrap_or(0),
String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stderr).trim(),
))
}
}
Loading

0 comments on commit ed180e5

Please sign in to comment.