Skip to content

Commit

Permalink
rust plugin: support projects with workspaces
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
Chris Patterson committed Dec 12, 2019
1 parent 6e8a232 commit 3c6c12d
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 47 deletions.
87 changes: 73 additions & 14 deletions snapcraft/plugins/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,15 @@
import collections
import logging
import os
from pathlib import Path
from contextlib import suppress
from typing import List, Optional

import toml

import snapcraft
from snapcraft import sources
from snapcraft import shell_utils
from snapcraft import file_utils, shell_utils
from snapcraft.internal import errors

_RUSTUP = "https://sh.rustup.rs/"
Expand Down Expand Up @@ -188,21 +189,74 @@ def _get_target(self) -> str:
)
return rust_target.format("unknown-linux", "gnu")

def _project_uses_workspace(self) -> bool:
path = Path(self.builddir, "Cargo.toml")
if not path.is_file():
return False

config = open(path).read()
return "workspace" in toml.loads(config)

def _install_workspace_artifacts(self) -> None:
"""Install workspace artifacts."""
# Find artifacts in release directory.
release_dir = Path(self.builddir, "target", "release")

# Install binaries to bin/.
bins_dir = Path(self.installdir, "bin")
bins_dir.mkdir(parents=True, exist_ok=True)

# Install shared objects to usr/lib/<arch-triplet>.
# TODO: Dynamic library support needs to be properly added.
# Although weinstall libraries if we find them, they are most
# likely going to be missing dependencies, e.g.:
# /home/ubuntu/.cargo/toolchains/stable-x86_64-unknown-linux-gnu/lib/libstd-fae576517123aa4e.so
libs_dir = Path(self.installdir, "usr", "lib", self.project.arch_triplet)
libs_dir.mkdir(parents=True, exist_ok=True)

# Cargo build marks binaries and shared objects executable...
# Search target directory to get these and install them to the
# correct location.
for path in release_dir.iterdir():
if not os.path.isfile(path):
continue
if not os.access(path, os.X_OK):
continue

# File is executable, now to determine if bin or lib...
if path.name.endswith(".so"):
file_utils.link_or_copy(path.as_posix(), libs_dir.as_posix())
else:
file_utils.link_or_copy(path.as_posix(), bins_dir.as_posix())

def build(self):
super().build()

# Write a minimal config.
self._write_cargo_config()

install_cmd = [
self._cargo_cmd,
"install",
"--path",
self.builddir,
"--root",
self.installdir,
"--force",
]
uses_workspaces = self._project_uses_workspace()

if uses_workspaces:
# This is a bit ugly because `cargo install` does not yet support
# workspaces. Alternatively, there is a perhaps better option
# to use `cargo-build --out-dir`, but `--out-dir` is considered
# unstable and unavailable for use yet on the stable channel. It
# may be better because the use of `cargo install` without `--locked`
# does not appear to honor Cargo.lock, while `cargo build` does by
# default, if it is present.
install_cmd = [self._cargo_cmd, "build", "--release"]
else:
# Write a minimal config.
self._write_cargo_config()

install_cmd = [
self._cargo_cmd,
"install",
"--path",
self.builddir,
"--root",
self.installdir,
"--force",
]

toolchain = self._get_toolchain()
if toolchain is not None:
install_cmd.insert(1, "+{}".format(toolchain))
Expand All @@ -218,11 +272,16 @@ def build(self):
install_cmd.append(" ".join(self.options.rust_features))

# build and install.
self.run(install_cmd, env=self._build_env())
self.run(install_cmd, env=self._build_env(), cwd=self.builddir)

# Finally, record.
self._record_manifest()

if uses_workspaces:
# We need to install the workspace artifacts as a workaround until
# `cargo build` supports `out-dir` in "stable".
self._install_workspace_artifacts()

def _build_env(self):
env = os.environ.copy()

Expand Down
88 changes: 55 additions & 33 deletions tests/unit/plugins/test_rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import collections
from pathlib import Path
import os
import subprocess
import textwrap
Expand Down Expand Up @@ -189,11 +190,7 @@ def test_cross_compile(self, mock_download):
env=plugin._build_env(),
),
mock.call(
[
plugin._cargo_cmd,
"+stable",
"fetch",
],
[plugin._cargo_cmd, "+stable", "fetch"],
cwd=plugin.builddir,
env=plugin._build_env(),
),
Expand Down Expand Up @@ -254,10 +251,7 @@ def test_cross_compile_with_rust_toolchain_file(self, mock_download):
env=plugin._build_env(),
),
mock.call(
[
plugin._cargo_cmd,
"fetch",
],
[plugin._cargo_cmd, "fetch"],
cwd=plugin.builddir,
env=plugin._build_env(),
),
Expand Down Expand Up @@ -340,11 +334,7 @@ def test_pull(self, script_mock):
env=plugin._build_env(),
),
mock.call(
[
plugin._cargo_cmd,
"+stable",
"fetch",
],
[plugin._cargo_cmd, "+stable", "fetch"],
cwd=plugin.builddir,
env=plugin._build_env(),
),
Expand Down Expand Up @@ -376,10 +366,7 @@ def test_pull_with_rust_toolchain_file(self, script_mock):
env=plugin._build_env(),
),
mock.call(
[
plugin._cargo_cmd,
"fetch",
],
[plugin._cargo_cmd, "fetch"],
cwd=plugin.builddir,
env=plugin._build_env(),
),
Expand Down Expand Up @@ -416,11 +403,7 @@ def test_pull_with_channel(self, script_mock):
env=plugin._build_env(),
),
mock.call(
[
plugin._cargo_cmd,
"+nightly",
"fetch",
],
[plugin._cargo_cmd, "+nightly", "fetch"],
cwd=plugin.builddir,
env=plugin._build_env(),
),
Expand Down Expand Up @@ -457,11 +440,7 @@ def test_pull_with_revision(self, script_mock):
env=plugin._build_env(),
),
mock.call(
[
plugin._cargo_cmd,
"+1.13.0",
"fetch",
],
[plugin._cargo_cmd, "+1.13.0", "fetch"],
cwd=plugin.builddir,
env=plugin._build_env(),
),
Expand Down Expand Up @@ -497,11 +476,7 @@ def test_pull_with_source_and_source_subdir(self, script_mock):
env=plugin._build_env(),
),
mock.call(
[
plugin._cargo_cmd,
"+stable",
"fetch",
],
[plugin._cargo_cmd, "+stable", "fetch"],
cwd=plugin.builddir,
env=plugin._build_env(),
),
Expand Down Expand Up @@ -536,6 +511,53 @@ def test_build(self):
env=plugin._build_env(),
)

def test_install_workspace_artifacts(self):
plugin = rust.RustPlugin("test-part", self.options, self.project)
release_path = Path(plugin.builddir, "target", "release")
os.makedirs(release_path, exist_ok=True)

p_nonexec = Path(release_path / "nonexec")
open(p_nonexec, "w").write("")
p_nonexec.chmod(0o664)

p_exec = Path(release_path / "exec")
open(p_exec, "w").write("")
p_exec.chmod(0o755)

p_exec_so = Path(release_path / "exec.so")
open(p_exec_so, "w").write("")
p_exec_so.chmod(0o755)

plugin._install_workspace_artifacts()

bindir = Path(plugin.installdir, "bin")
bins = list(bindir.iterdir())

libdir = Path(plugin.installdir, "usr", "lib", self.project.arch_triplet)
libs = list(libdir.iterdir())

self.assertThat(bins, Equals([bindir / "exec"]))
self.assertThat(libs, Equals([libdir / "exec.so"]))

def test_build_workspace(self):
plugin = rust.RustPlugin("test-part", self.options, self.project)
os.makedirs(plugin.sourcedir)

os.makedirs(plugin.builddir, exist_ok=True)
cargo_path = Path(plugin.builddir, "Cargo.toml")
with open(cargo_path, "w") as cargo_file:
cargo_file.write("[workspace]" + os.linesep)
release_path = Path(plugin.builddir, "target", "release")
os.makedirs(release_path, exist_ok=True)

plugin.build()

self.run_mock.assert_called_once_with(
["/home/ubuntu/.cargo/bin/cargo", "+stable", "build", "--release"],
cwd=os.path.join(plugin.partdir, "build"),
env=plugin._build_env(),
)

def test_build_with_rust_toolchain_file(self):
plugin = rust.RustPlugin("test-part", self.options, self.project)
os.makedirs(plugin.sourcedir)
Expand Down

0 comments on commit 3c6c12d

Please sign in to comment.