diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 054e15f7ebbc..200c6b4f67b5 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -15,6 +15,7 @@ import os import pickle import re +import shlex import subprocess import typing as T @@ -106,7 +107,7 @@ def gcc_rsp_quote(s: str) -> str: # variables (or variables we use them in) is interpreted directly by ninja # (e.g. the value of the depfile variable is a pathname that ninja will read # from, etc.), so it must not be shell quoted. -raw_names = {'DEPFILE_UNQUOTED', 'DESC', 'pool', 'description', 'targetdep', 'dyndep'} +raw_names = {'DEPFILE_UNQUOTED', 'DESC', 'pool', 'description', 'targetdep', 'dyndep', 'RUSTENV'} NINJA_QUOTE_BUILD_PAT = re.compile(r"[$ :\n]") NINJA_QUOTE_VAR_PAT = re.compile(r"[$ \n]") @@ -2282,6 +2283,8 @@ def generate_rust_target(self, target: build.BuildTarget, target_name: str, obj_ element.add_dep(deps) element.add_item('ARGS', args) element.add_item('targetdep', depfile) + if target.rust_compile_env: + element.add_item('RUSTENV', self._rust_env_tokens(target.rust_compile_env)) self.add_build(element) self.create_target_source_introspection(target, rustc, args, [main_rust_file], []) @@ -2605,9 +2608,27 @@ def generate_cython_compile_rules(self, compiler: 'Compiler') -> None: depfile=depfile, restat=True)) + def _rust_env_tokens(self, env: T.Dict[str, str]) -> T.List[str]: + ''' + Build the tokens that expand into the $RUSTENV ninja variable. They + are pre-quoted for the target shell so they survive ninja + substitution without further escaping + ''' + if mesonlib.is_windows(): + wrapper = self.environment.meson_env_exe + assert wrapper is not None, \ + 'meson_env.exe path not set; setup-time build is missing' + tokens = [mesonlib.quote_arg(wrapper)] + tokens += [mesonlib.quote_arg(f'{k}={v}') for k, v in env.items()] + return tokens + return [f'{k}={shlex.quote(v)}' for k, v in env.items()] + def generate_rust_compile_rules(self, compiler: RustCompiler) -> None: rule = self.compiler_to_rule_name(compiler) - command = compiler.get_exelist() + ['$ARGS', '$in'] + command: T.List[T.Union[str, NinjaCommandArg]] = [ + NinjaCommandArg('$RUSTENV', Quoting.none), + ] + command += compiler.get_exelist() + ['$ARGS', '$in'] description = 'Compiling Rust source $in' depfile = '$targetdep' depstyle = 'gcc' diff --git a/mesonbuild/build.py b/mesonbuild/build.py index ba61e4ddc35a..bbbeef451913 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -904,6 +904,8 @@ def __init__( self.implicit_include_directories = kwargs.get('implicit_include_directories', True) self.gnu_symbol_visibility = kwargs.get('gnu_symbol_visibility', '') self.rust_dependency_map = kwargs.get('rust_dependency_map', {}) + # Env vars to set for rustc + self.rust_compile_env: T.Dict[str, str] = {} self.swift_interoperability_mode = kwargs.get('swift_interoperability_mode', 'c') self.swift_module_name = kwargs.get('swift_module_name') or self.name diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py index e1e30e35dcca..dc44432a2a07 100644 --- a/mesonbuild/cargo/interpreter.py +++ b/mesonbuild/cargo/interpreter.py @@ -16,6 +16,7 @@ import os import pathlib import collections +import subprocess import urllib.parse import typing as T from pathlib import PurePath @@ -47,6 +48,41 @@ def _dependency_name(package_name: str, api: str, suffix: str = '-rs') -> str: return f'{basename}-{api}{suffix}' +def ensure_meson_env_exe(environment: Environment) -> None: + """ + Build the meson_env wrapper on windows when using stable Rust that lacks + --env-set, since that's the only way to set a process's environment in + ninja. Requires a build-machine Rust toolchain. + """ + if environment.meson_env_exe is not None: + return + from ..mesonlib import is_windows + if not is_windows(): + return + host_rust = environment.coredata.compilers[MachineChoice.HOST].get('rust') + if host_rust is None: + return + if T.cast('RustCompiler', host_rust).enable_env_set_args() is not None: + # rustc supports --env-set; no wrapper needed. + return + + build_rust = environment.coredata.compilers[MachineChoice.BUILD].get('rust') + if build_rust is None: + raise MesonException('Cargo subproject on Windows requires a build-machine Rust toolchain') + + here = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + source = os.path.join(here, 'scripts', 'meson_env.rs') + out = os.path.join(environment.get_scratch_dir(), 'meson_env.exe') + if not os.path.exists(out) or os.path.getmtime(out) < os.path.getmtime(source): + mlog.log('Building meson_env wrapper for Cargo subprojects...') + cmd = build_rust.get_exelist() + ['-O', '-o', out, source] + try: + subprocess.run(cmd, check=True) + except subprocess.CalledProcessError as e: + raise MesonException(f'Failed to build meson_env wrapper: {e}') + environment.meson_env_exe = out + + def _extra_args_varname() -> str: return 'extra_args' @@ -167,7 +203,7 @@ def get_lint_args(self, rustc: RustCompiler) -> T.List[str]: return args - def get_env_args(self, rustc: RustCompiler, environment: Environment, subdir: str) -> T.List[str]: + def get_env_set_args(self, rustc: RustCompiler, environment: Environment, subdir: str) -> T.List[str]: """Get environment variable arguments for rustc.""" enable_env_set_args = rustc.enable_env_set_args() if enable_env_set_args is None: @@ -179,6 +215,15 @@ def get_env_args(self, rustc: RustCompiler, environment: Environment, subdir: st env_args.extend(['--env-set', f'{k}={v}']) return env_args + def get_rustc_env(self, environment: Environment, subdir: str, machine: MachineChoice) -> T.Dict[str, str]: + """Get environment variables as a dict for rustc.""" + if not environment.is_cross_build(): + machine = MachineChoice.HOST + rustc = T.cast('RustCompiler', environment.coredata.compilers[machine]['rust']) + if rustc.enable_env_set_args() is not None: + return {} + return self.get_env_dict(environment, subdir) + def get_rustc_args(self, environment: Environment, subdir: str, machine: MachineChoice) -> T.List[str]: """Get rustc arguments for this package.""" if not environment.is_cross_build(): @@ -191,7 +236,7 @@ def get_rustc_args(self, environment: Environment, subdir: str, machine: Machine args: T.List[str] = [] args.extend(self.get_lint_args(rustc)) args.extend(cfg.get_features_args()) - args.extend(self.get_env_args(rustc, environment, subdir)) + args.extend(self.get_env_set_args(rustc, environment, subdir)) return args def supported_abis(self) -> T.Set[RUST_ABI]: diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py index de5daa48b4fb..5365f2048e27 100644 --- a/mesonbuild/environment.py +++ b/mesonbuild/environment.py @@ -85,6 +85,10 @@ class Environment: def __init__(self, source_dir: str, build_dir: T.Optional[str], cmd_options: cmdline.SharedCMDOptions) -> None: self.source_dir = source_dir + # Path to the meson_env wrapper executable built at setup time. + # Currently only used when a Cargo workspace is being interpreted on + # Windows with stable Rust. + self.meson_env_exe: T.Optional[str] = None # Do not try to create build directories when build_dir is none. # This reduced mode is used by the --buildoptions introspector if build_dir is not None: diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 7f623385ff2b..ec402555631b 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -342,6 +342,22 @@ def merge_kw_args(self, state: ModuleState, kwargs: T.Union[RustPackageExecutabl kwargs['override_options'].setdefault('rust_std', self.package.manifest.package.edition) + def _apply_rustc_env(self, state: ModuleState, + native: MachineChoice, + result: T.Union[BothLibraries, BuildTarget]) -> None: + env = self.package.get_rustc_env(state.environment, state.subdir, native) + if not env: + return + # On Windows we need a tiny native wrapper to set the env before + # running rustc + from .. import cargo as _cargo + _cargo.interpreter.ensure_meson_env_exe(state.environment) + if isinstance(result, BothLibraries): + result.shared.rust_compile_env = dict(env) + result.static.rust_compile_env = dict(env) + else: + result.rust_compile_env = dict(env) + def _library_method(self, state: ModuleState, args: T.Tuple[ T.Optional[T.Union[str, StructuredSources]], T.Optional[StructuredSources]], kwargs: RustPackageLibrary, @@ -366,22 +382,25 @@ def _library_method(self, state: ModuleState, args: T.Tuple[ lib_args: T.Tuple[str, SourcesVarargsType] = (tgt_name, [sources]) self.merge_kw_args(state, kwargs) + native = kwargs['native'] + result: T.Union[BothLibraries, SharedLibrary, StaticLibrary, SharedModule] if shared_mod: - return state._interpreter.build_target(state.current_node, lib_args, - T.cast('_kwargs.SharedModule', kwargs), - SharedModule) - - if static and shared: - return state._interpreter.build_both_libraries(state.current_node, lib_args, kwargs) + result = state._interpreter.build_target(state.current_node, lib_args, + T.cast('_kwargs.SharedModule', kwargs), + SharedModule) + elif static and shared: + result = state._interpreter.build_both_libraries(state.current_node, lib_args, kwargs) elif shared: - return state._interpreter.build_target(state.current_node, lib_args, - T.cast('_kwargs.SharedLibrary', kwargs), - SharedLibrary) + result = state._interpreter.build_target(state.current_node, lib_args, + T.cast('_kwargs.SharedLibrary', kwargs), + SharedLibrary) else: - return state._interpreter.build_target(state.current_node, lib_args, - T.cast('_kwargs.StaticLibrary', kwargs), - StaticLibrary) + result = state._interpreter.build_target(state.current_node, lib_args, + T.cast('_kwargs.StaticLibrary', kwargs), + StaticLibrary) + self._apply_rustc_env(state, native, result) + return result def _proc_macro_method(self, state: 'ModuleState', args: T.Tuple[ T.Optional[T.Union[str, StructuredSources]], @@ -519,7 +538,10 @@ def executable_method(self, state: 'ModuleState', args: T.Tuple[ exe_args: T.Tuple[str, SourcesVarargsType] = (tgt_name, [sources]) self.merge_kw_args(state, kwargs) - return state._interpreter.build_target(state.current_node, exe_args, kwargs, Executable) + native = kwargs['native'] + result = state._interpreter.build_target(state.current_node, exe_args, kwargs, Executable) + self._apply_rustc_env(state, native, result) + return result class RustSubproject(RustCrate): diff --git a/mesonbuild/scripts/meson_env.rs b/mesonbuild/scripts/meson_env.rs new file mode 100644 index 000000000000..88934414af54 --- /dev/null +++ b/mesonbuild/scripts/meson_env.rs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright The Meson development team + +// Tiny helper used by Meson on Windows to set environment variables before +// executing a command, equivalent to `env(1)` on POSIX systems. +// +// Usage: meson_env KEY1=VAL1 [KEY2=VAL2 ...] COMMAND [ARG ...] +// +// All leading arguments that look like `K=V` (and where K is a valid env-var +// name) are consumed as assignments; the first non-assignment argument and +// everything after it becomes the command to execute. The child process' +// exit code is propagated. + +use std::env; +use std::process::{exit, Command}; + +fn is_assignment(arg: &str) -> Option<(&str, &str)> { + let (k, v) = arg.split_once('=')?; + if k.is_empty() { + return None; + } + let first = k.as_bytes()[0]; + if !(first.is_ascii_alphabetic() || first == b'_') { + return None; + } + if !k.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') { + return None; + } + Some((k, v)) +} + +fn main() { + let mut args = env::args().skip(1); + let mut cmd: Option = None; + while let Some(arg) = args.next() { + match is_assignment(&arg) { + Some((k, v)) => env::set_var(k, v), + None => { + cmd = Some(arg); + break; + } + } + } + let cmd = match cmd { + Some(c) => c, + None => { + eprintln!("meson_env: no command given"); + exit(2); + } + }; + let rest: Vec = args.collect(); + let status = match Command::new(&cmd).args(&rest).status() { + Ok(s) => s, + Err(e) => { + eprintln!("meson_env: failed to execute {cmd}: {e}"); + exit(127); + } + }; + exit(status.code().unwrap_or(1)); +} diff --git a/test cases/rust/43 cargo env vars/main.c b/test cases/rust/43 cargo env vars/main.c new file mode 100644 index 000000000000..decf4c1ca168 --- /dev/null +++ b/test cases/rust/43 cargo env vars/main.c @@ -0,0 +1,5 @@ +int check_envs(void); + +int main(int argc, char *argv[]) { + return check_envs(); +} diff --git a/test cases/rust/43 cargo env vars/meson.build b/test cases/rust/43 cargo env vars/meson.build new file mode 100644 index 000000000000..8e0f90172fb3 --- /dev/null +++ b/test cases/rust/43 cargo env vars/meson.build @@ -0,0 +1,5 @@ +project('cargo env vars', 'c') + +envcheck_dep = dependency('envcheck-1') +exe = executable('app', 'main.c', dependencies: envcheck_dep) +test('cargo env vars', exe) diff --git a/test cases/rust/43 cargo env vars/subprojects/envcheck-1-rs.wrap b/test cases/rust/43 cargo env vars/subprojects/envcheck-1-rs.wrap new file mode 100644 index 000000000000..61dbad77964e --- /dev/null +++ b/test cases/rust/43 cargo env vars/subprojects/envcheck-1-rs.wrap @@ -0,0 +1,5 @@ +[wrap-file] +method = cargo + +[provide] +dependency_names = envcheck-1 diff --git a/test cases/rust/43 cargo env vars/subprojects/envcheck-1-rs/Cargo.toml b/test cases/rust/43 cargo env vars/subprojects/envcheck-1-rs/Cargo.toml new file mode 100644 index 000000000000..c8e9bb1f8b2c --- /dev/null +++ b/test cases/rust/43 cargo env vars/subprojects/envcheck-1-rs/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "envcheck" +version = "1.2.3" +edition = "2021" + +[lib] +crate-type = ["staticlib"] +path = "lib.rs" diff --git a/test cases/rust/43 cargo env vars/subprojects/envcheck-1-rs/lib.rs b/test cases/rust/43 cargo env vars/subprojects/envcheck-1-rs/lib.rs new file mode 100644 index 000000000000..a451b3431571 --- /dev/null +++ b/test cases/rust/43 cargo env vars/subprojects/envcheck-1-rs/lib.rs @@ -0,0 +1,17 @@ +const PKG_NAME: &str = env!("CARGO_PKG_NAME"); +const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); +const PKG_VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR"); +const PKG_VERSION_MINOR: &str = env!("CARGO_PKG_VERSION_MINOR"); +const PKG_VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH"); +const CRATE_NAME: &str = env!("CARGO_CRATE_NAME"); + +#[no_mangle] +pub extern "C" fn check_envs() -> i32 { + if PKG_NAME != "envcheck" { return 1; } + if PKG_VERSION != "1.2.3" { return 2; } + if PKG_VERSION_MAJOR != "1" { return 3; } + if PKG_VERSION_MINOR != "2" { return 4; } + if PKG_VERSION_PATCH != "3" { return 5; } + if CRATE_NAME != "envcheck" { return 6; } + 0 +}