Skip to content

Commit fa3fe36

Browse files
author
Chris Patterson
committed
rust plugin: support projects with workspaces
Unfortunately, `cargo install` does not support workspaces yet: rust-lang/cargo#7599 Alternatively, we use `cargo build`, which will respect the Cargo.lock configuration. It however, does require a bit more hoop jumping to determine which binaries were built and install them. Introudce _install_workspace_artifacts() to install the built executables into the correct paths. Testing has covered executables and libraries, though dynamic linking is not quite yet supported by the rust plugin (at least in my testing, it will have unmnet dependencies on libstd-<id>.so). We can address that feature gap in the future, but likely doesn't affect snap users because they are probably using the standard linking process which doesn't require libstd (likely due to static linking of those dependencies). `cargo build` has an unstable flag option for `--out-dir` which may simplifiy the install process, but is currently unavailable for stable use: https://doc.rust-lang.org/cargo/reference/unstable.html#out-dir Add/update tests for coverage. Signed-off-by: Chris Patterson <[email protected]>
1 parent cd0c8b5 commit fa3fe36

File tree

2 files changed

+128
-47
lines changed

2 files changed

+128
-47
lines changed

snapcraft/plugins/rust.py

+73-14
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,15 @@
4545
import collections
4646
import logging
4747
import os
48+
from pathlib import Path
4849
from contextlib import suppress
4950
from typing import List, Optional
5051

5152
import toml
5253

5354
import snapcraft
5455
from snapcraft import sources
55-
from snapcraft import shell_utils
56+
from snapcraft import file_utils, shell_utils
5657
from snapcraft.internal import errors
5758

5859
_RUSTUP = "https://sh.rustup.rs/"
@@ -183,21 +184,74 @@ def _get_target(self) -> str:
183184
)
184185
return rust_target.format("unknown-linux", "gnu")
185186

187+
def _project_uses_workspace(self) -> bool:
188+
path = Path(self.builddir, "Cargo.toml")
189+
if not path.is_file():
190+
return False
191+
192+
config = open(path).read()
193+
return "workspace" in toml.loads(config)
194+
195+
def _install_workspace_artifacts(self) -> None:
196+
"""Install workspace artifacts."""
197+
# Find artifacts in release directory.
198+
release_dir = Path(self.builddir, "target", "release")
199+
200+
# Install binaries to bin/.
201+
bins_dir = Path(self.installdir, "bin")
202+
bins_dir.mkdir(parents=True, exist_ok=True)
203+
204+
# Install shared objects to usr/lib/<arch-triplet>.
205+
# TODO: Dynamic library support needs to be properly added.
206+
# Although weinstall libraries if we find them, they are most
207+
# likely going to be missing dependencies, e.g.:
208+
# /home/ubuntu/.cargo/toolchains/stable-x86_64-unknown-linux-gnu/lib/libstd-fae576517123aa4e.so
209+
libs_dir = Path(self.installdir, "usr", "lib", self.project.arch_triplet)
210+
libs_dir.mkdir(parents=True, exist_ok=True)
211+
212+
# Cargo build marks binaries and shared objects executable...
213+
# Search target directory to get these and install them to the
214+
# correct location.
215+
for path in release_dir.iterdir():
216+
if not os.path.isfile(path):
217+
continue
218+
if not os.access(path, os.X_OK):
219+
continue
220+
221+
# File is executable, now to determine if bin or lib...
222+
if path.name.endswith(".so"):
223+
file_utils.link_or_copy(path.as_posix(), libs_dir.as_posix())
224+
else:
225+
file_utils.link_or_copy(path.as_posix(), bins_dir.as_posix())
226+
186227
def build(self):
187228
super().build()
188229

189-
# Write a minimal config.
190-
self._write_cargo_config()
191-
192-
install_cmd = [
193-
self._cargo_cmd,
194-
"install",
195-
"--path",
196-
self.builddir,
197-
"--root",
198-
self.installdir,
199-
"--force",
200-
]
230+
uses_workspaces = self._project_uses_workspace()
231+
232+
if uses_workspaces:
233+
# This is a bit ugly because `cargo install` does not yet support
234+
# workspaces. Alternatively, there is a perhaps better option
235+
# to use `cargo-build --out-dir`, but `--out-dir` is considered
236+
# unstable and unavailable for use yet on the stable channel. It
237+
# may be better because the use of `cargo install` without `--locked`
238+
# does not appear to honor Cargo.lock, while `cargo build` does by
239+
# default, if it is present.
240+
install_cmd = [self._cargo_cmd, "build", "--release"]
241+
else:
242+
# Write a minimal config.
243+
self._write_cargo_config()
244+
245+
install_cmd = [
246+
self._cargo_cmd,
247+
"install",
248+
"--path",
249+
self.builddir,
250+
"--root",
251+
self.installdir,
252+
"--force",
253+
]
254+
201255
toolchain = self._get_toolchain()
202256
if toolchain is not None:
203257
install_cmd.insert(1, "+{}".format(toolchain))
@@ -213,11 +267,16 @@ def build(self):
213267
install_cmd.append(" ".join(self.options.rust_features))
214268

215269
# build and install.
216-
self.run(install_cmd, env=self._build_env())
270+
self.run(install_cmd, env=self._build_env(), cwd=self.builddir)
217271

218272
# Finally, record.
219273
self._record_manifest()
220274

275+
if uses_workspaces:
276+
# We need to install the workspace artifacts as a workaround until
277+
# `cargo build` supports `out-dir` in "stable".
278+
self._install_workspace_artifacts()
279+
221280
def _build_env(self):
222281
env = os.environ.copy()
223282

tests/unit/plugins/test_rust.py

+55-33
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

1818
import collections
19+
from pathlib import Path
1920
import os
2021
import subprocess
2122
import textwrap
@@ -189,11 +190,7 @@ def test_cross_compile(self, mock_download):
189190
env=plugin._build_env(),
190191
),
191192
mock.call(
192-
[
193-
plugin._cargo_cmd,
194-
"+stable",
195-
"fetch",
196-
],
193+
[plugin._cargo_cmd, "+stable", "fetch"],
197194
cwd=plugin.builddir,
198195
env=plugin._build_env(),
199196
),
@@ -254,10 +251,7 @@ def test_cross_compile_with_rust_toolchain_file(self, mock_download):
254251
env=plugin._build_env(),
255252
),
256253
mock.call(
257-
[
258-
plugin._cargo_cmd,
259-
"fetch",
260-
],
254+
[plugin._cargo_cmd, "fetch"],
261255
cwd=plugin.builddir,
262256
env=plugin._build_env(),
263257
),
@@ -340,11 +334,7 @@ def test_pull(self, script_mock):
340334
env=plugin._build_env(),
341335
),
342336
mock.call(
343-
[
344-
plugin._cargo_cmd,
345-
"+stable",
346-
"fetch",
347-
],
337+
[plugin._cargo_cmd, "+stable", "fetch"],
348338
cwd=plugin.builddir,
349339
env=plugin._build_env(),
350340
),
@@ -376,10 +366,7 @@ def test_pull_with_rust_toolchain_file(self, script_mock):
376366
env=plugin._build_env(),
377367
),
378368
mock.call(
379-
[
380-
plugin._cargo_cmd,
381-
"fetch",
382-
],
369+
[plugin._cargo_cmd, "fetch"],
383370
cwd=plugin.builddir,
384371
env=plugin._build_env(),
385372
),
@@ -416,11 +403,7 @@ def test_pull_with_channel(self, script_mock):
416403
env=plugin._build_env(),
417404
),
418405
mock.call(
419-
[
420-
plugin._cargo_cmd,
421-
"+nightly",
422-
"fetch",
423-
],
406+
[plugin._cargo_cmd, "+nightly", "fetch"],
424407
cwd=plugin.builddir,
425408
env=plugin._build_env(),
426409
),
@@ -457,11 +440,7 @@ def test_pull_with_revision(self, script_mock):
457440
env=plugin._build_env(),
458441
),
459442
mock.call(
460-
[
461-
plugin._cargo_cmd,
462-
"+1.13.0",
463-
"fetch",
464-
],
443+
[plugin._cargo_cmd, "+1.13.0", "fetch"],
465444
cwd=plugin.builddir,
466445
env=plugin._build_env(),
467446
),
@@ -497,11 +476,7 @@ def test_pull_with_source_and_source_subdir(self, script_mock):
497476
env=plugin._build_env(),
498477
),
499478
mock.call(
500-
[
501-
plugin._cargo_cmd,
502-
"+stable",
503-
"fetch",
504-
],
479+
[plugin._cargo_cmd, "+stable", "fetch"],
505480
cwd=plugin.builddir,
506481
env=plugin._build_env(),
507482
),
@@ -536,6 +511,53 @@ def test_build(self):
536511
env=plugin._build_env(),
537512
)
538513

514+
def test_install_workspace_artifacts(self):
515+
plugin = rust.RustPlugin("test-part", self.options, self.project)
516+
release_path = Path(plugin.builddir, "target", "release")
517+
os.makedirs(release_path, exist_ok=True)
518+
519+
p_nonexec = Path(release_path / "nonexec")
520+
open(p_nonexec, "w").write("")
521+
p_nonexec.chmod(0o664)
522+
523+
p_exec = Path(release_path / "exec")
524+
open(p_exec, "w").write("")
525+
p_exec.chmod(0o755)
526+
527+
p_exec_so = Path(release_path / "exec.so")
528+
open(p_exec_so, "w").write("")
529+
p_exec_so.chmod(0o755)
530+
531+
plugin._install_workspace_artifacts()
532+
533+
bindir = Path(plugin.installdir, "bin")
534+
bins = list(bindir.iterdir())
535+
536+
libdir = Path(plugin.installdir, "usr", "lib", self.project.arch_triplet)
537+
libs = list(libdir.iterdir())
538+
539+
self.assertThat(bins, Equals([bindir / "exec"]))
540+
self.assertThat(libs, Equals([libdir / "exec.so"]))
541+
542+
def test_build_workspace(self):
543+
plugin = rust.RustPlugin("test-part", self.options, self.project)
544+
os.makedirs(plugin.sourcedir)
545+
546+
os.makedirs(plugin.builddir, exist_ok=True)
547+
cargo_path = Path(plugin.builddir, "Cargo.toml")
548+
with open(cargo_path, "w") as cargo_file:
549+
cargo_file.write("[workspace]" + os.linesep)
550+
release_path = Path(plugin.builddir, "target", "release")
551+
os.makedirs(release_path, exist_ok=True)
552+
553+
plugin.build()
554+
555+
self.run_mock.assert_called_once_with(
556+
[plugin._cargo_cmd, "+stable", "build", "--release"],
557+
cwd=os.path.join(plugin.partdir, "build"),
558+
env=plugin._build_env(),
559+
)
560+
539561
def test_build_with_rust_toolchain_file(self):
540562
plugin = rust.RustPlugin("test-part", self.options, self.project)
541563
os.makedirs(plugin.sourcedir)

0 commit comments

Comments
 (0)