Skip to content

Commit ff64bbc

Browse files
committed
cargo: Pass env vars in the env when --env-set is not available
This means using the documented method to set env vars[1] when invoking a process on POSIX, and using a tiny Rust wrapper on Windows, where `cmd /c` is used to spawn processes. This Rust wrapper is built during setup (only on Windows with a non-nightly Rust) when a cargo workspace is used. Of course, this means a build-machine Rust toolchain is required in such cases, but that's already a requirement in Cargo due to build.rs 1. https://ninja-build.org/manual.html#ref_rule_command
1 parent cf0c0cf commit ff64bbc

11 files changed

Lines changed: 211 additions & 17 deletions

File tree

mesonbuild/backend/ninjabackend.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import os
1616
import pickle
1717
import re
18+
import shlex
1819
import subprocess
1920
import typing as T
2021

@@ -106,7 +107,7 @@ def gcc_rsp_quote(s: str) -> str:
106107
# variables (or variables we use them in) is interpreted directly by ninja
107108
# (e.g. the value of the depfile variable is a pathname that ninja will read
108109
# from, etc.), so it must not be shell quoted.
109-
raw_names = {'DEPFILE_UNQUOTED', 'DESC', 'pool', 'description', 'targetdep', 'dyndep'}
110+
raw_names = {'DEPFILE_UNQUOTED', 'DESC', 'pool', 'description', 'targetdep', 'dyndep', 'RUSTENV'}
110111

111112
NINJA_QUOTE_BUILD_PAT = re.compile(r"[$ :\n]")
112113
NINJA_QUOTE_VAR_PAT = re.compile(r"[$ \n]")
@@ -2282,6 +2283,8 @@ def generate_rust_target(self, target: build.BuildTarget, target_name: str, obj_
22822283
element.add_dep(deps)
22832284
element.add_item('ARGS', args)
22842285
element.add_item('targetdep', depfile)
2286+
if target.rust_compile_env:
2287+
element.add_item('RUSTENV', self._rust_env_tokens(target.rust_compile_env))
22852288
self.add_build(element)
22862289
self.create_target_source_introspection(target, rustc, args, [main_rust_file], [])
22872290

@@ -2605,9 +2608,27 @@ def generate_cython_compile_rules(self, compiler: 'Compiler') -> None:
26052608
depfile=depfile,
26062609
restat=True))
26072610

2611+
def _rust_env_tokens(self, env: T.Dict[str, str]) -> T.List[str]:
2612+
'''
2613+
Build the tokens that expand into the $RUSTENV ninja variable. They
2614+
are pre-quoted for the target shell so they survive ninja
2615+
substitution without further escaping
2616+
'''
2617+
if mesonlib.is_windows():
2618+
wrapper = self.environment.meson_env_exe
2619+
assert wrapper is not None, \
2620+
'meson_env.exe path not set; setup-time build is missing'
2621+
tokens = [mesonlib.quote_arg(wrapper)]
2622+
tokens += [mesonlib.quote_arg(f'{k}={v}') for k, v in env.items()]
2623+
return tokens
2624+
return [f'{k}={shlex.quote(v)}' for k, v in env.items()]
2625+
26082626
def generate_rust_compile_rules(self, compiler: RustCompiler) -> None:
26092627
rule = self.compiler_to_rule_name(compiler)
2610-
command = compiler.get_exelist() + ['$ARGS', '$in']
2628+
command: T.List[T.Union[str, NinjaCommandArg]] = [
2629+
NinjaCommandArg('$RUSTENV', Quoting.none),
2630+
]
2631+
command += compiler.get_exelist() + ['$ARGS', '$in']
26112632
description = 'Compiling Rust source $in'
26122633
depfile = '$targetdep'
26132634
depstyle = 'gcc'

mesonbuild/build.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,8 @@ def __init__(
904904
self.implicit_include_directories = kwargs.get('implicit_include_directories', True)
905905
self.gnu_symbol_visibility = kwargs.get('gnu_symbol_visibility', '')
906906
self.rust_dependency_map = kwargs.get('rust_dependency_map', {})
907+
# Env vars to set for rustc
908+
self.rust_compile_env: T.Dict[str, str] = {}
907909

908910
self.swift_interoperability_mode = kwargs.get('swift_interoperability_mode', 'c')
909911
self.swift_module_name = kwargs.get('swift_module_name') or self.name

mesonbuild/cargo/interpreter.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import os
1717
import pathlib
1818
import collections
19+
import subprocess
1920
import urllib.parse
2021
import typing as T
2122
from pathlib import PurePath
@@ -47,6 +48,41 @@ def _dependency_name(package_name: str, api: str, suffix: str = '-rs') -> str:
4748
return f'{basename}-{api}{suffix}'
4849

4950

51+
def ensure_meson_env_exe(environment: Environment) -> None:
52+
"""
53+
Build the meson_env wrapper on windows when using stable Rust that lacks
54+
--env-set, since that's the only way to set a process's environment in
55+
ninja. Requires a build-machine Rust toolchain.
56+
"""
57+
if environment.meson_env_exe is not None:
58+
return
59+
from ..mesonlib import is_windows
60+
if not is_windows():
61+
return
62+
host_rust = environment.coredata.compilers[MachineChoice.HOST].get('rust')
63+
if host_rust is None:
64+
return
65+
if T.cast('RustCompiler', host_rust).enable_env_set_args() is not None:
66+
# rustc supports --env-set; no wrapper needed.
67+
return
68+
69+
build_rust = environment.coredata.compilers[MachineChoice.BUILD].get('rust')
70+
if build_rust is None:
71+
raise MesonException('Cargo subproject on Windows requires a build-machine Rust toolchain')
72+
73+
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
74+
source = os.path.join(here, 'scripts', 'meson_env.rs')
75+
out = os.path.join(environment.get_scratch_dir(), 'meson_env.exe')
76+
if not os.path.exists(out) or os.path.getmtime(out) < os.path.getmtime(source):
77+
mlog.log('Building meson_env wrapper for Cargo subprojects...')
78+
cmd = build_rust.get_exelist() + ['-O', '-o', out, source]
79+
try:
80+
subprocess.run(cmd, check=True)
81+
except subprocess.CalledProcessError as e:
82+
raise MesonException(f'Failed to build meson_env wrapper: {e}')
83+
environment.meson_env_exe = out
84+
85+
5086
def _extra_args_varname() -> str:
5187
return 'extra_args'
5288

@@ -167,7 +203,7 @@ def get_lint_args(self, rustc: RustCompiler) -> T.List[str]:
167203

168204
return args
169205

170-
def get_env_args(self, rustc: RustCompiler, environment: Environment, subdir: str) -> T.List[str]:
206+
def get_env_set_args(self, rustc: RustCompiler, environment: Environment, subdir: str) -> T.List[str]:
171207
"""Get environment variable arguments for rustc."""
172208
enable_env_set_args = rustc.enable_env_set_args()
173209
if enable_env_set_args is None:
@@ -179,6 +215,15 @@ def get_env_args(self, rustc: RustCompiler, environment: Environment, subdir: st
179215
env_args.extend(['--env-set', f'{k}={v}'])
180216
return env_args
181217

218+
def get_rustc_env(self, environment: Environment, subdir: str, machine: MachineChoice) -> T.Dict[str, str]:
219+
"""Get environment variables as a dict for rustc."""
220+
if not environment.is_cross_build():
221+
machine = MachineChoice.HOST
222+
rustc = T.cast('RustCompiler', environment.coredata.compilers[machine]['rust'])
223+
if rustc.enable_env_set_args() is not None:
224+
return {}
225+
return self.get_env_dict(environment, subdir)
226+
182227
def get_rustc_args(self, environment: Environment, subdir: str, machine: MachineChoice) -> T.List[str]:
183228
"""Get rustc arguments for this package."""
184229
if not environment.is_cross_build():
@@ -191,7 +236,7 @@ def get_rustc_args(self, environment: Environment, subdir: str, machine: Machine
191236
args: T.List[str] = []
192237
args.extend(self.get_lint_args(rustc))
193238
args.extend(cfg.get_features_args())
194-
args.extend(self.get_env_args(rustc, environment, subdir))
239+
args.extend(self.get_env_set_args(rustc, environment, subdir))
195240
return args
196241

197242
def supported_abis(self) -> T.Set[RUST_ABI]:

mesonbuild/environment.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ class Environment:
8585

8686
def __init__(self, source_dir: str, build_dir: T.Optional[str], cmd_options: cmdline.SharedCMDOptions) -> None:
8787
self.source_dir = source_dir
88+
# Path to the meson_env wrapper executable built at setup time.
89+
# Currently only used when a Cargo workspace is being interpreted on
90+
# Windows with stable Rust.
91+
self.meson_env_exe: T.Optional[str] = None
8892
# Do not try to create build directories when build_dir is none.
8993
# This reduced mode is used by the --buildoptions introspector
9094
if build_dir is not None:

mesonbuild/modules/rust.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,22 @@ def merge_kw_args(self, state: ModuleState, kwargs: T.Union[RustPackageExecutabl
342342

343343
kwargs['override_options'].setdefault('rust_std', self.package.manifest.package.edition)
344344

345+
def _apply_rustc_env(self, state: ModuleState,
346+
native: MachineChoice,
347+
result: T.Union[BothLibraries, BuildTarget]) -> None:
348+
env = self.package.get_rustc_env(state.environment, state.subdir, native)
349+
if not env:
350+
return
351+
# On Windows we need a tiny native wrapper to set the env before
352+
# running rustc
353+
from .. import cargo as _cargo
354+
_cargo.interpreter.ensure_meson_env_exe(state.environment)
355+
if isinstance(result, BothLibraries):
356+
result.shared.rust_compile_env = dict(env)
357+
result.static.rust_compile_env = dict(env)
358+
else:
359+
result.rust_compile_env = dict(env)
360+
345361
def _library_method(self, state: ModuleState, args: T.Tuple[
346362
T.Optional[T.Union[str, StructuredSources]],
347363
T.Optional[StructuredSources]], kwargs: RustPackageLibrary,
@@ -366,22 +382,25 @@ def _library_method(self, state: ModuleState, args: T.Tuple[
366382

367383
lib_args: T.Tuple[str, SourcesVarargsType] = (tgt_name, [sources])
368384
self.merge_kw_args(state, kwargs)
385+
native = kwargs['native']
369386

387+
result: T.Union[BothLibraries, SharedLibrary, StaticLibrary, SharedModule]
370388
if shared_mod:
371-
return state._interpreter.build_target(state.current_node, lib_args,
372-
T.cast('_kwargs.SharedModule', kwargs),
373-
SharedModule)
374-
375-
if static and shared:
376-
return state._interpreter.build_both_libraries(state.current_node, lib_args, kwargs)
389+
result = state._interpreter.build_target(state.current_node, lib_args,
390+
T.cast('_kwargs.SharedModule', kwargs),
391+
SharedModule)
392+
elif static and shared:
393+
result = state._interpreter.build_both_libraries(state.current_node, lib_args, kwargs)
377394
elif shared:
378-
return state._interpreter.build_target(state.current_node, lib_args,
379-
T.cast('_kwargs.SharedLibrary', kwargs),
380-
SharedLibrary)
395+
result = state._interpreter.build_target(state.current_node, lib_args,
396+
T.cast('_kwargs.SharedLibrary', kwargs),
397+
SharedLibrary)
381398
else:
382-
return state._interpreter.build_target(state.current_node, lib_args,
383-
T.cast('_kwargs.StaticLibrary', kwargs),
384-
StaticLibrary)
399+
result = state._interpreter.build_target(state.current_node, lib_args,
400+
T.cast('_kwargs.StaticLibrary', kwargs),
401+
StaticLibrary)
402+
self._apply_rustc_env(state, native, result)
403+
return result
385404

386405
def _proc_macro_method(self, state: 'ModuleState', args: T.Tuple[
387406
T.Optional[T.Union[str, StructuredSources]],
@@ -519,7 +538,10 @@ def executable_method(self, state: 'ModuleState', args: T.Tuple[
519538

520539
exe_args: T.Tuple[str, SourcesVarargsType] = (tgt_name, [sources])
521540
self.merge_kw_args(state, kwargs)
522-
return state._interpreter.build_target(state.current_node, exe_args, kwargs, Executable)
541+
native = kwargs['native']
542+
result = state._interpreter.build_target(state.current_node, exe_args, kwargs, Executable)
543+
self._apply_rustc_env(state, native, result)
544+
return result
523545

524546

525547
class RustSubproject(RustCrate):

mesonbuild/scripts/meson_env.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright The Meson development team
3+
4+
// Tiny helper used by Meson on Windows to set environment variables before
5+
// executing a command, equivalent to `env(1)` on POSIX systems.
6+
//
7+
// Usage: meson_env KEY1=VAL1 [KEY2=VAL2 ...] COMMAND [ARG ...]
8+
//
9+
// All leading arguments that look like `K=V` (and where K is a valid env-var
10+
// name) are consumed as assignments; the first non-assignment argument and
11+
// everything after it becomes the command to execute. The child process'
12+
// exit code is propagated.
13+
14+
use std::env;
15+
use std::process::{exit, Command};
16+
17+
fn is_assignment(arg: &str) -> Option<(&str, &str)> {
18+
let (k, v) = arg.split_once('=')?;
19+
if k.is_empty() {
20+
return None;
21+
}
22+
let first = k.as_bytes()[0];
23+
if !(first.is_ascii_alphabetic() || first == b'_') {
24+
return None;
25+
}
26+
if !k.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
27+
return None;
28+
}
29+
Some((k, v))
30+
}
31+
32+
fn main() {
33+
let mut args = env::args().skip(1);
34+
let mut cmd: Option<String> = None;
35+
while let Some(arg) = args.next() {
36+
match is_assignment(&arg) {
37+
Some((k, v)) => env::set_var(k, v),
38+
None => {
39+
cmd = Some(arg);
40+
break;
41+
}
42+
}
43+
}
44+
let cmd = match cmd {
45+
Some(c) => c,
46+
None => {
47+
eprintln!("meson_env: no command given");
48+
exit(2);
49+
}
50+
};
51+
let rest: Vec<String> = args.collect();
52+
let status = match Command::new(&cmd).args(&rest).status() {
53+
Ok(s) => s,
54+
Err(e) => {
55+
eprintln!("meson_env: failed to execute {cmd}: {e}");
56+
exit(127);
57+
}
58+
};
59+
exit(status.code().unwrap_or(1));
60+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
int check_envs(void);
2+
3+
int main(int argc, char *argv[]) {
4+
return check_envs();
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
project('cargo env vars', 'c')
2+
3+
envcheck_dep = dependency('envcheck-1')
4+
exe = executable('app', 'main.c', dependencies: envcheck_dep)
5+
test('cargo env vars', exe)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[wrap-file]
2+
method = cargo
3+
4+
[provide]
5+
dependency_names = envcheck-1
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "envcheck"
3+
version = "1.2.3"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["staticlib"]
8+
path = "lib.rs"

0 commit comments

Comments
 (0)