Skip to content

Commit 6d11cb1

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 949f909 commit 6d11cb1

File tree

2 files changed

+120
-13
lines changed

2 files changed

+120
-13
lines changed

snapcraft/plugins/rust.py

Lines changed: 72 additions & 13 deletions
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/"
@@ -194,21 +195,74 @@ def _get_target(self) -> str:
194195
)
195196
return rust_target.format("unknown-linux", "gnu")
196197

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

200-
# Write a minimal config.
201-
self._write_cargo_config()
241+
uses_workspaces = self._project_uses_workspace()
242+
243+
if uses_workspaces:
244+
# This is a bit ugly because `cargo install` does not yet support
245+
# workspaces. Alternatively, there is a perhaps better option
246+
# to use `cargo-build --out-dir`, but `--out-dir` is considered
247+
# unstable and unavailable for use yet on the stable channel. It
248+
# may be better because the use of `cargo install` without `--locked`
249+
# does not appear to honor Cargo.lock, while `cargo build` does by
250+
# default, if it is present.
251+
install_cmd = [self._cargo_cmd, "build", "--release"]
252+
else:
253+
# Write a minimal config.
254+
self._write_cargo_config()
255+
256+
install_cmd = [
257+
self._cargo_cmd,
258+
"install",
259+
"--path",
260+
self.builddir,
261+
"--root",
262+
self.installdir,
263+
"--force",
264+
]
202265

203-
install_cmd = [
204-
self._cargo_cmd,
205-
"install",
206-
"--path",
207-
self.builddir,
208-
"--root",
209-
self.installdir,
210-
"--force",
211-
]
212266
toolchain = self._get_toolchain()
213267
if toolchain is not None:
214268
install_cmd.insert(1, "+{}".format(toolchain))
@@ -224,11 +278,16 @@ def build(self):
224278
install_cmd.append(" ".join(self.options.rust_features))
225279

226280
# build and install.
227-
self.run(install_cmd, env=self._build_env())
281+
self.run(install_cmd, env=self._build_env(), cwd=self.builddir)
228282

229283
# Finally, record.
230284
self._record_manifest()
231285

286+
if uses_workspaces:
287+
# We need to install the workspace artifacts as a workaround until
288+
# `cargo build` supports `out-dir` in "stable".
289+
self._install_workspace_artifacts()
290+
232291
def _build_env(self):
233292
env = os.environ.copy()
234293

tests/unit/plugins/test_rust.py

Lines changed: 48 additions & 0 deletions
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
@@ -550,6 +551,53 @@ def test_build(self):
550551
env=plugin._build_env(),
551552
)
552553

554+
def test_install_workspace_artifacts(self):
555+
plugin = rust.RustPlugin("test-part", self.options, self.project)
556+
release_path = Path(plugin.builddir, "target", "release")
557+
os.makedirs(release_path, exist_ok=True)
558+
559+
p_nonexec = Path(release_path / "nonexec")
560+
open(p_nonexec, "w").write("")
561+
p_nonexec.chmod(0o664)
562+
563+
p_exec = Path(release_path / "exec")
564+
open(p_exec, "w").write("")
565+
p_exec.chmod(0o755)
566+
567+
p_exec_so = Path(release_path / "exec.so")
568+
open(p_exec_so, "w").write("")
569+
p_exec_so.chmod(0o755)
570+
571+
plugin._install_workspace_artifacts()
572+
573+
bindir = Path(plugin.installdir, "bin")
574+
bins = list(bindir.iterdir())
575+
576+
libdir = Path(plugin.installdir, "usr", "lib", self.project.arch_triplet)
577+
libs = list(libdir.iterdir())
578+
579+
self.assertThat(bins, Equals([bindir / "exec"]))
580+
self.assertThat(libs, Equals([libdir / "exec.so"]))
581+
582+
def test_build_workspace(self):
583+
plugin = rust.RustPlugin("test-part", self.options, self.project)
584+
os.makedirs(plugin.sourcedir)
585+
586+
os.makedirs(plugin.builddir, exist_ok=True)
587+
cargo_path = Path(plugin.builddir, "Cargo.toml")
588+
with open(cargo_path, "w") as cargo_file:
589+
cargo_file.write("[workspace]" + os.linesep)
590+
release_path = Path(plugin.builddir, "target", "release")
591+
os.makedirs(release_path, exist_ok=True)
592+
593+
plugin.build()
594+
595+
self.run_mock.assert_called_once_with(
596+
[plugin._cargo_cmd, "+stable", "build", "--release"],
597+
cwd=os.path.join(plugin.partdir, "build"),
598+
env=plugin._build_env(),
599+
)
600+
553601
def test_build_with_rust_toolchain_file(self):
554602
plugin = rust.RustPlugin("test-part", self.options, self.project)
555603
os.makedirs(plugin.sourcedir)

0 commit comments

Comments
 (0)