Skip to content

Commit

Permalink
Apple: Rebuild when deployment target changes
Browse files Browse the repository at this point in the history
Rebuild when the `*_DEPLOYMENT_TARGET` variables change when building
Apple targets.

This is done by:
1. Adding it as a tracked variable to `Options` to make sure it clears
   the incremental cache.
2. Emitting the variable in `--emit=dep-info` (`.d`) files, so that
   Cargo can pick up changes to it, and correctly trigger a rebuild.
  • Loading branch information
madsmtm committed Aug 21, 2024
1 parent 7fc9179 commit fb6ccd3
Show file tree
Hide file tree
Showing 12 changed files with 212 additions and 48 deletions.
25 changes: 15 additions & 10 deletions compiler/rustc_interface/src/passes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,7 @@ fn escape_dep_filename(filename: &str) -> String {

// Makefile comments only need escaping newlines and `\`.
// The result can be unescaped by anything that can unescape `escape_default` and friends.
fn escape_dep_env(symbol: Symbol) -> String {
let s = symbol.as_str();
fn escape_dep_env(s: &str) -> String {
let mut escaped = String::with_capacity(s.len());
for c in s.chars() {
match c {
Expand Down Expand Up @@ -492,16 +491,22 @@ fn write_out_deps(tcx: TyCtxt<'_>, outputs: &OutputFilenames, out_filenames: &[P

// Emit special comments with information about accessed environment variables.
let env_depinfo = sess.psess.env_depinfo.borrow();

// We will soon sort, so the initial order does not matter.
#[allow(rustc::potential_query_instability)]
let mut env_depinfo: Vec<_> = env_depinfo
.iter()
.map(|(k, v)| (escape_dep_env(k.as_str()), v.map(|v| escape_dep_env(v.as_str()))))
.chain(tcx.sess.target.is_like_osx.then(|| {
// On Apple targets, we also depend on the deployment target environment variable.
let name = rustc_target::spec::apple_deployment_target_env(&tcx.sess.target.os);
(name.into(), std::env::var(name).ok().map(|var| escape_dep_env(&var)))
}))
.collect();
env_depinfo.sort_unstable();
if !env_depinfo.is_empty() {
// We will soon sort, so the initial order does not matter.
#[allow(rustc::potential_query_instability)]
let mut envs: Vec<_> = env_depinfo
.iter()
.map(|(k, v)| (escape_dep_env(*k), v.map(escape_dep_env)))
.collect();
envs.sort_unstable();
writeln!(file)?;
for (k, v) in envs {
for (k, v) in env_depinfo {
write!(file, "# env-dep:{k}")?;
if let Some(v) = v {
write!(file, "={v}")?;
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_session/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,7 @@ impl Default for Options {
color: ColorConfig::Auto,
logical_env: FxIndexMap::default(),
verbose: false,
apple_deployment_target_env: None,
}
}
}
Expand Down Expand Up @@ -2710,6 +2711,7 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M
color,
logical_env,
verbose,
apple_deployment_target_env: None,
}
}

Expand Down
9 changes: 9 additions & 0 deletions compiler/rustc_session/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,15 @@ top_level_options!(
color: ColorConfig [UNTRACKED],

verbose: bool [TRACKED_NO_CRATE_HASH],

/// The raw value of the `*_DEPLOYMENT_TARGET` environment variable
/// for the selected target OS.
///
/// The exact environment variable to use depends on the target that
/// the user has chosen, and we do not want to re-compile if an
/// unrelated deployment target environment variable changed, so we
/// defer the initialization of this to `build_session`.
apple_deployment_target_env: Option<String> [TRACKED],
}
);

Expand Down
9 changes: 8 additions & 1 deletion compiler/rustc_session/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1006,7 +1006,7 @@ fn default_emitter(
#[allow(rustc::untranslatable_diagnostic)] // FIXME: make this translatable
pub fn build_session(
early_dcx: EarlyDiagCtxt,
sopts: config::Options,
mut sopts: config::Options,
io: CompilerIO,
bundle: Option<Lrc<rustc_errors::FluentBundle>>,
registry: rustc_errors::registry::Registry,
Expand Down Expand Up @@ -1110,6 +1110,13 @@ pub fn build_session(

let asm_arch = if target.allow_asm { InlineAsmArch::from_str(&target.arch).ok() } else { None };

// Configure the deployment target for change-tracking, now that target
// details are available.
if target.is_like_osx {
let name = rustc_target::spec::apple_deployment_target_env(&target.os);
sopts.apple_deployment_target_env = std::env::var(name).ok();
}

let sess = Session {
target,
host,
Expand Down
31 changes: 20 additions & 11 deletions compiler/rustc_target/src/spec/base/apple/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,19 @@ pub fn platform(target: &Target) -> Option<u32> {
})
}

/// Name of the environment variable used to fetch the deployment target on
/// the given OS.
pub fn deployment_target_env(os: &str) -> &'static str {
match os {
"macos" => "MACOSX_DEPLOYMENT_TARGET",
"ios" => "IPHONEOS_DEPLOYMENT_TARGET",
"watchos" => "WATCHOS_DEPLOYMENT_TARGET",
"tvos" => "TVOS_DEPLOYMENT_TARGET",
"visionos" => "XROS_DEPLOYMENT_TARGET",
_ => unreachable!("tried to get deployment target env var for non-Apple platform"),
}
}

/// Hack for calling `deployment_target` outside of this module.
pub fn deployment_target_for_target(target: &Target) -> (u16, u8, u8) {
let arch = if target.llvm_target.starts_with("arm64e") {
Expand Down Expand Up @@ -323,17 +336,13 @@ fn deployment_target(os: &str, arch: Arch, abi: TargetAbi) -> (u16, u8, u8) {
_ => os_min,
};

// The environment variable used to fetch the deployment target.
let env_var = match os {
"macos" => "MACOSX_DEPLOYMENT_TARGET",
"ios" => "IPHONEOS_DEPLOYMENT_TARGET",
"watchos" => "WATCHOS_DEPLOYMENT_TARGET",
"tvos" => "TVOS_DEPLOYMENT_TARGET",
"visionos" => "XROS_DEPLOYMENT_TARGET",
_ => unreachable!("tried to get deployment target env var for non-Apple platform"),
};

if let Ok(deployment_target) = env::var(env_var) {
// NOTE: We access the deployment target environment variable here, which
// makes the variable an **implicit** input which affects compilation.
//
// We make sure to rebuild when the variable changes, both by busting the
// incremental cache, and by telling Cargo that it is a dependency.
// Search for usages of `deployment_target_env` to see how.
if let Ok(deployment_target) = env::var(deployment_target_env(os)) {
match parse_version(&deployment_target) {
// It is common that the deployment target is set too low, e.g. on
// macOS Aarch64 to also target older x86_64, the user may set a
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_target/src/spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub mod crt_objects;

mod base;
pub use base::apple::{
deployment_target_env as apple_deployment_target_env,
deployment_target_for_target as current_apple_deployment_target,
platform as current_apple_platform, sdk_version as current_apple_sdk_version,
};
Expand Down
2 changes: 1 addition & 1 deletion src/tools/run-make-support/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ pub use env::{env_var, env_var_os};
pub use run::{cmd, run, run_fail, run_with_args};

/// Helpers for checking target information.
pub use targets::{is_darwin, is_msvc, is_windows, llvm_components_contain, target, uname};
pub use targets::{is_darwin, is_msvc, is_windows, llvm_components_contain, target, uname, apple_os};

/// Helpers for building names of output artifacts that are potentially target-specific.
pub use artifact_names::{
Expand Down
18 changes: 18 additions & 0 deletions src/tools/run-make-support/src/targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ pub fn is_darwin() -> bool {
target().contains("darwin")
}

/// Get the target OS on Apple operating systems.
#[must_use]
pub fn apple_os() -> &'static str {
if target().contains("darwin") {
"macos"
} else if target().contains("ios") {
"ios"
} else if target().contains("tvos") {
"tvos"
} else if target().contains("watchos") {
"watchos"
} else if target().contains("visionos") {
"visionos"
} else {
panic!("not an Apple OS")
}
}

/// Check if `component` is within `LLVM_COMPONENTS`
#[must_use]
pub fn llvm_components_contain(component: &str) -> bool {
Expand Down
1 change: 1 addition & 0 deletions tests/run-make/apple-deployment-target/foo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
137 changes: 137 additions & 0 deletions tests/run-make/apple-deployment-target/rmake.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//! Test codegen when setting deployment targets on Apple platforms.
//!
//! This is important since its a compatibility hazard. The linker will
//! generate load commands differently based on what minimum OS it can assume.
//!
//! See https://github.com/rust-lang/rust/pull/105123.

//@ only-apple

use run_make_support::{apple_os, cmd, rustc, target};

/// Run vtool to check the `minos` field in LC_BUILD_VERSION.
///
/// On lower deployment targets, LC_VERSION_MIN_MACOSX, LC_VERSION_MIN_IPHONEOS and similar
/// are used instead of LC_BUILD_VERSION - these have a `version` field, so also check that.
#[track_caller]
fn minos(file: &str, version: &str) {
cmd("vtool")
.arg("-show-build")
.arg(file)
.run()
.assert_stdout_contains_regex(format!("(minos|version) {version}"));
}

fn main() {
// These versions should generally be higher than the default versions
let (env_var, example_version, higher_example_version) = match apple_os() {
"macos" => ("MACOSX_DEPLOYMENT_TARGET", "12.0", "13.0"),
// armv7s-apple-ios and i386-apple-ios only supports iOS 10.0
"ios" if target() == "armv7s-apple-ios" || target() == "i386-apple-ios" => ("IPHONEOS_DEPLOYMENT_TARGET", "10.0", "10.0"),
"ios" => ("IPHONEOS_DEPLOYMENT_TARGET", "15.0", "16.0"),
"watchos" => ("WATCHOS_DEPLOYMENT_TARGET", "7.0", "9.0"),
"tvos" => ("TVOS_DEPLOYMENT_TARGET", "14.0", "15.0"),
"visionos" => ("XROS_DEPLOYMENT_TARGET", "1.1", "1.2"),
_ => unreachable!(),
};
let default_version =
rustc().target(target()).env_remove(env_var).print("deployment-target").run().stdout_utf8();
let default_version = default_version.strip_prefix("deployment_target=").unwrap().trim();

// Test that version makes it to the object file.
{
let mut rustc = rustc();
rustc.target(target());
rustc.crate_type("lib");
rustc.emit("obj");
rustc.input("foo.rs");
rustc.output("foo.o");

rustc.env(env_var, example_version).run();
minos("foo.o", example_version);

// FIXME(madsmtm): Doesn't work on Mac Catalyst and the simulator.
if !target().contains("macabi") && !target().contains("sim") {
rustc.env_remove(env_var).run();
minos("foo.o", default_version);
}
}

// Test that version makes it to the linker when linking dylibs.
//
// Certain watchOS targets don't support dynamic linking, so we disable the test on those.
if apple_os() != "watchos" {
let mut rustc = rustc();
rustc.target(target());
rustc.crate_type("dylib");
rustc.input("foo.rs");
rustc.output("libfoo.dylib");

rustc.env(env_var, example_version).run();
minos("libfoo.dylib", example_version);

// FIXME(madsmtm): Deployment target is not currently passed properly to linker
// rustc.env_remove(env_var).run();
// minos("libfoo.dylib", default_version);

// Test with ld64 instead
rustc.arg("-Clinker-flavor=ld");

rustc.env(env_var, example_version).run();
minos("libfoo.dylib", example_version);

rustc.env_remove(env_var).run();
minos("libfoo.dylib", default_version);
}

// Test that version makes it to the linker when linking executables.
{
let mut rustc = rustc();
rustc.target(target());
rustc.crate_type("bin");
rustc.input("foo.rs");
rustc.output("foo");

// FIXME(madsmtm): Doesn't work on watchOS for some reason?
if !target().contains("watchos") {
rustc.env(env_var, example_version).run();
minos("foo", example_version);
}

// FIXME(madsmtm): Deployment target is not currently passed properly to linker
// rustc.env_remove(env_var).run();
// minos("foo", default_version);

// Test with ld64 instead
rustc.arg("-Clinker-flavor=ld");

rustc.env(env_var, example_version).run();
minos("foo", example_version);

rustc.env_remove(env_var).run();
minos("foo", default_version);
}

// Test that changing the deployment target busts the incremental cache.
{
let mut rustc = rustc();
rustc.target(target());
rustc.incremental("incremental");
rustc.crate_type("lib");
rustc.emit("obj");
rustc.input("foo.rs");
rustc.output("foo.o");

rustc.env(env_var, example_version).run();
minos("foo.o", example_version);

rustc.env(env_var, higher_example_version).run();
minos("foo.o", higher_example_version);

// FIXME(madsmtm): Doesn't work on Mac Catalyst and the simulator.
if !target().contains("macabi") && !target().contains("sim") {
rustc.env_remove(env_var).run();
minos("foo.o", default_version);
}
}
}
21 changes: 0 additions & 21 deletions tests/run-make/macos-deployment-target/Makefile

This file was deleted.

This file was deleted.

0 comments on commit fb6ccd3

Please sign in to comment.