Skip to content

Commit dca4019

Browse files
committed
WIP: Add an unstable module for Rust Cargo support
Currently only supports building staticlib libraries and binaries with Cargo. All configuration must be in the Cargo.toml file. You should use cargo 0.23 because it fixes a bug in emitting dep-info FIXMEs: * Cross compilation is broken. We do not pass the `--target TRIPLE` flag to cargo, so it always builds targetting the build machine. * tests and benches are currently not supported, but can be added * cdylibs are not supported because it is not clear if anyone uses them and if they even work properly in Rust * `ninja dist` does not yet run `cargo vendor` to add crate sources * `cargo clean` is not called on `ninja clean` * We cannot handle adding system libraries that are needed by the staticlib while linking because it's outputted at build time by `cargo build`. You must handle that yourself. * We do not pass on RUSTFLAGS from the env during configure to cargo build.
1 parent 545d79b commit dca4019

File tree

14 files changed

+374
-0
lines changed

14 files changed

+374
-0
lines changed

mesonbuild/mesonlib.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,8 @@ def expand_arguments(args):
539539
return None
540540
return expended_args
541541

542+
Popen_safe_errors = (FileNotFoundError, PermissionError, subprocess.CalledProcessError)
543+
542544
def Popen_safe(args, write=None, stderr=subprocess.PIPE, **kwargs):
543545
if sys.version_info < (3, 6) or not sys.stdout.encoding:
544546
return Popen_safe_legacy(args, write=write, stderr=stderr, **kwargs)

mesonbuild/modules/unstable_cargo.py

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
# Copyright 2017 The Meson development team
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import json
17+
18+
from .. import mlog
19+
from ..mesonlib import Popen_safe, Popen_safe_errors
20+
from ..mesonlib import extract_as_list, version_compare, MesonException, File
21+
from ..environment import for_windows, for_cygwin, for_darwin
22+
from ..interpreterbase import noKwargs, permittedKwargs
23+
from ..dependencies import InternalDependency
24+
from ..build import CustomTarget
25+
26+
from . import ExtensionModule, ModuleReturnValue, permittedSnippetKwargs
27+
28+
target_kwargs = {'toml', 'sources', 'install', 'install_dir'}
29+
30+
31+
class CargoModule(ExtensionModule):
32+
cargo = ['cargo']
33+
cargo_build = ['cargo', 'build', '-v', '--color=always']
34+
cargo_version = None
35+
36+
def __init__(self):
37+
super().__init__()
38+
try:
39+
self._get_version()
40+
except Popen_safe_errors:
41+
raise MesonException('Cargo was not found')
42+
self.snippets.add('test')
43+
self.snippets.add('benchmark')
44+
45+
def _get_cargo_build(self, toml):
46+
# FIXME: must use target-triple to set the host system, currently it
47+
# always builds for the build machine.
48+
return self.cargo_build + ['--manifest-path', toml]
49+
50+
def _is_release(self, env):
51+
buildtype = env.coredata.get_builtin_option('buildtype')
52+
return not buildtype.startswith('debug')
53+
54+
def _get_crate_name(self, name, crate_type, env):
55+
'''
56+
We have no control over what filenames cargo uses for its output, so we
57+
have to figure it out ourselves.
58+
'''
59+
if for_cygwin(env.is_cross_build(), env):
60+
raise MesonException('Cygwin cargo support is TODO')
61+
prefix = 'lib'
62+
if crate_type == 'staticlib':
63+
suffix = 'a'
64+
if for_windows(env.is_cross_build(), env):
65+
prefix = ''
66+
suffix = 'lib'
67+
elif crate_type == 'cdylib':
68+
if for_windows(env.is_cross_build(), env):
69+
prefix = ''
70+
suffix = 'dll'
71+
elif for_darwin(env.is_cross_build(), env):
72+
suffix = 'dylib'
73+
else:
74+
suffix = 'so'
75+
elif crate_type == 'bin':
76+
prefix = ''
77+
suffix = ''
78+
if for_windows(env.is_cross_build(), env):
79+
suffix = 'exe'
80+
if suffix:
81+
fname = prefix + name + '.' + suffix
82+
else:
83+
fname = prefix + name
84+
if self._is_release(env):
85+
return os.path.join('release', fname)
86+
else:
87+
return os.path.join('debug', fname)
88+
89+
def _read_metadata(self, toml):
90+
cmd = self.cargo + ['metadata', '--format-version=1', '--no-deps',
91+
'--manifest-path', toml]
92+
out = Popen_safe(cmd)[1]
93+
try:
94+
encoded = json.loads(out)
95+
except json.decoder.JSONDecodeError:
96+
print(cmd, out)
97+
raise
98+
return encoded['packages'][0]
99+
100+
def _source_strings_to_files(self, source_dir, subdir, sources):
101+
results = []
102+
for s in sources:
103+
if isinstance(s, File):
104+
pass
105+
elif isinstance(s, str):
106+
s = File.from_source_file(source_dir, subdir, s)
107+
else:
108+
raise MesonException('Source item is {!r} instead of '
109+
'string or files() object'.format(s))
110+
results.append(s)
111+
return results
112+
113+
def _get_sources(self, state, kwargs):
114+
# 'sources' kwargs is optional; we have a depfile with dependency
115+
# information and ninja will use that to determine when to rebuild.
116+
sources = extract_as_list(kwargs, 'sources')
117+
return self._source_strings_to_files(state.environment.source_dir,
118+
state.subdir, sources)
119+
120+
def _get_cargo_test_outputs(self, name, metadata, env):
121+
args = []
122+
outputs = []
123+
depfile = None
124+
for t in metadata['targets']:
125+
if t['name'] != name:
126+
continue
127+
# Filter out crate types we don't want
128+
# a test target will only have one output
129+
if t['crate_types'] != ['bin']:
130+
continue
131+
# Filter out the target `kind`s that we don't want
132+
if t['kind'] != ['test']:
133+
continue
134+
outputs.append(self._get_crate_name(name, 'bin', env))
135+
args = ['--test', name]
136+
break
137+
if outputs:
138+
depfile = os.path.splitext(outputs[0])[0] + '.d'
139+
else:
140+
toml = metadata['manifest_path']
141+
raise MesonException('no test called {!r} found in {!r}'
142+
''.format(name, toml))
143+
return outputs, depfile, args
144+
145+
def _get_cargo_executable_outputs(self, name, metadata, env):
146+
args = []
147+
outputs = []
148+
depfile = None
149+
for t in metadata['targets']:
150+
if t['name'] != name:
151+
continue
152+
# Filter out crate types we don't want
153+
# an executable target will only have one output
154+
if t['crate_types'] != ['bin']:
155+
continue
156+
# Filter out the target `kind`s that we don't want
157+
if t['kind'] not in [['example'], ['bin']]:
158+
continue
159+
outputs.append(self._get_crate_name(name, 'bin', env))
160+
if t['kind'][0] == 'example':
161+
args = ['--example', name]
162+
else:
163+
args = ['--bin', name]
164+
break
165+
if outputs:
166+
depfile = os.path.splitext(outputs[0])[0] + '.d'
167+
else:
168+
toml = metadata['manifest_path']
169+
raise MesonException('no bin called {!r} found in {!r}'
170+
''.format(name, toml))
171+
return outputs, depfile, args
172+
173+
def _get_cargo_static_library_outputs(self, name, metadata, env):
174+
args = []
175+
outputs = []
176+
depfile = None
177+
for t in metadata['targets']:
178+
if t['name'] != name:
179+
continue
180+
# Filter out the target `kind`s that we don't want
181+
# a library target can have multiple outputs
182+
if 'staticlib' not in t['kind'] and \
183+
'example' not in t['kind']:
184+
continue
185+
for ct in t['crate_types']:
186+
if ct == 'staticlib':
187+
outputs.append(self._get_crate_name(name, ct, env))
188+
if t['kind'][0] == 'example':
189+
# If the library is an example, it must be built by name
190+
args = ['--example', name]
191+
else:
192+
# Library is the crate itself, no name needed
193+
args = ['--lib']
194+
break
195+
if outputs:
196+
depfile = os.path.splitext(outputs[0])[0] + '.d'
197+
else:
198+
toml = metadata['manifest_path']
199+
raise MesonException('no staticlib called {!r} found '
200+
'in {!r}'.format(name, toml))
201+
return outputs, depfile, args
202+
203+
def _get_cargo_outputs(self, name, metadata, env, cargo_target_type):
204+
# FIXME: track which outputs have already been fetched from
205+
# a toml file and disallow duplicates.
206+
fn = getattr(self, '_get_cargo_{}_outputs'.format(cargo_target_type))
207+
return fn(name, metadata, env)
208+
209+
def _check_cargo_dep_info_bug(self, metadata):
210+
if version_compare(self.cargo_version, '>0.22.0'):
211+
return
212+
for t in metadata['targets']:
213+
if t['kind'] == ['custom-build']:
214+
m = 'Crate {!r} contains a custom build script {!r} which ' \
215+
'will cause dep-info to not being emitted due to a ' \
216+
'bug in Cargo. Please upgrade to Cargo 0.23 or newer.' \
217+
''.format(metadata['name'], os.path.basename(t['src_path']))
218+
mlog.warning(m)
219+
return
220+
221+
def _cargo_target(self, state, args, kwargs, cargo_target_type):
222+
ctkwargs = {}
223+
env = state.environment
224+
if len(args) != 1:
225+
raise MesonException('{0}() requires exactly one positional '
226+
'argument: the name of the {0}'
227+
''.format(cargo_target_type))
228+
name = args[0]
229+
if 'toml' not in kwargs:
230+
raise MesonException('"toml" kwarg is required')
231+
toml = File.from_source_file(env.get_source_dir(), state.subdir,
232+
kwargs['toml'])
233+
# Get the Cargo.toml file as a JSON encoded object
234+
md = self._read_metadata(toml.absolute_path(env.source_dir, None))
235+
# Warn about the cargo dep-info bug if needed
236+
self._check_cargo_dep_info_bug(md)
237+
# Get the list of outputs that cargo will create matching the specified name
238+
ctkwargs['output'], ctkwargs['depfile'], cargo_args = \
239+
self._get_cargo_outputs(name, md, env, cargo_target_type)
240+
# Set the files that will trigger a rebuild
241+
ctkwargs['depend_files'] = [toml] + self._get_sources(state, kwargs)
242+
# Cargo command that will build the output library/libraries/bins
243+
cmd = self._get_cargo_build(toml) + cargo_args
244+
if self._is_release(env):
245+
cmd.append('--release')
246+
ctkwargs['command'] = cmd
247+
if 'install' in kwargs:
248+
ctkwargs['install'] = kwargs['install']
249+
if 'install_dir' in kwargs:
250+
ctkwargs['install_dir'] = kwargs['install_dir']
251+
elif 'install' in kwargs:
252+
# People should be able to set `install: true` and get a good
253+
# default for `install_dir`
254+
if cargo_target_type == 'static_library':
255+
ctkwargs['install_dir'] = env.coredata.get_builtin_option('libdir')
256+
elif cargo_target_type == 'executable':
257+
ctkwargs['install_dir'] = env.coredata.get_builtin_option('bindir')
258+
ct = CustomTarget(name, state.subdir, state.subproject, ctkwargs)
259+
# Ninja buffers all cargo output so we get no status updates
260+
ct.ninja_pool = 'console'
261+
# Force it to output in the current directory
262+
ct.envvars['CARGO_TARGET_DIR'] = state.subdir
263+
# XXX: we need to call `cargo clean` on `ninja clean`.
264+
return md, ct
265+
266+
@permittedKwargs(target_kwargs)
267+
def static_library(self, state, args, kwargs):
268+
md, ct = self._cargo_target(state, args, kwargs, 'static_library')
269+
# XXX: Cargo build outputs a list of system libraries that are needed
270+
# by this (possibly) static library, but we have no way of accessing it
271+
# during configure. So we require developers to manage that themselves.
272+
# XXX: We add the output file into `sources`, but that creates
273+
# a compile-time dependency instead of a link-time dependency and
274+
# reduces parallelism.
275+
d = InternalDependency(md['version'], [], [], [], [], [ct], [])
276+
return ModuleReturnValue(d, [d])
277+
278+
@permittedKwargs(target_kwargs)
279+
def executable(self, state, args, kwargs):
280+
md, ct = self._cargo_target(state, args, kwargs, 'executable')
281+
# XXX: We return a custom target, which means this may not be usable
282+
# everywhere that an executable build target can be.
283+
return ModuleReturnValue(ct, [ct])
284+
285+
@permittedSnippetKwargs(target_kwargs)
286+
def test(self, interpreter, state, args, kwargs):
287+
# This would map to cargo tests
288+
raise MesonException('Not implemented')
289+
290+
@permittedSnippetKwargs(target_kwargs)
291+
def benchmark(self, interpreter, state, args, kwargs):
292+
# This would map to cargo benches
293+
raise MesonException('Not implemented')
294+
295+
def _get_version(self):
296+
if self.cargo_version is None:
297+
out = Popen_safe(self.cargo + ['--version'])[1]
298+
self.cargo_version = out.strip().split('cargo ')[1]
299+
return self.cargo_version
300+
301+
@noKwargs
302+
def version(self, state, args, kwargs):
303+
return ModuleReturnValue(self._get_version(), [])
304+
305+
306+
def initialize():
307+
return CargoModule()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Cargo.lock
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "mesonbin"
3+
version = "0.1.0"
4+
authors = ["Nirbheek Chauhan <[email protected]>"]
5+
6+
[dependencies]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
cargo.executable('mesonbin',
2+
toml : 'Cargo.toml',
3+
# This is optional, since ninja keeps track of dependencies on source files.
4+
sources : ['src/main.rs'],
5+
install : true)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
println!("Hello, world!");
3+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "mesonbothlib"
3+
version = "0.1.0"
4+
authors = ["Nirbheek Chauhan <[email protected]>"]
5+
6+
[dependencies]
7+
8+
[lib]
9+
crate-type = ['staticlib', 'cdylib']
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
cargo.static_library('mesonbothlib',
2+
toml : 'Cargo.toml',
3+
install : true)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#[cfg(test)]
2+
mod tests {
3+
#[test]
4+
fn it_works() {
5+
assert_eq!(2 + 2, 4);
6+
}
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
?gcc:usr/lib/libmesonstatic.a
2+
?gcc:usr/lib/libmesonbothlib.a
3+
?msvc:usr/lib/mesonstatic.lib
4+
?msvc:usr/lib/mesonbothlib.lib
5+
usr/bin/mesonbin?exe
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
project('cargo module', 'c', 'rust')
2+
3+
cargo = import('unstable-cargo')
4+
5+
subdir('staticlib')
6+
subdir('bothlibs')
7+
subdir('bins')
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "mesonstatic"
3+
version = "0.1.0"
4+
authors = ["Nirbheek Chauhan <[email protected]>"]
5+
6+
[dependencies]
7+
8+
[lib]
9+
crate-type = ['staticlib']
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
cargo.static_library('mesonstatic',
2+
toml : 'Cargo.toml',
3+
install : true)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#[cfg(test)]
2+
mod tests {
3+
#[test]
4+
fn it_works() {
5+
assert_eq!(2 + 2, 4);
6+
}
7+
}

0 commit comments

Comments
 (0)