Skip to content

Commit

Permalink
Merge branch 'main' into ros-spread-test
Browse files Browse the repository at this point in the history
  • Loading branch information
mr-cal committed Sep 21, 2023
2 parents 16195a5 + b357ef5 commit 1ae07ec
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 57 deletions.
164 changes: 127 additions & 37 deletions snapcraft_legacy/plugins/v2/rust.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2020 Canonical Ltd
# Copyright (C) 2020-2023 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
Expand All @@ -14,7 +14,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""This rust plugin is useful for building rust based parts.
"""A Snapcraft plugin for Rust applications.
This Rust plugin is useful for building Rust based parts.
Rust uses cargo to drive the build.
Expand All @@ -23,22 +25,44 @@
'sources' topic for the latter.
Additionally, this plugin uses the following plugin-specific keywords:
- rust-channel
(string, default "stable")
Used to select which Rust channel or version to use.
It can be one of "stable", "beta", "nightly" or a version number.
If you don't want this plugin to install Rust toolchain for you,
you can put "none" for this option.
- rust-features
(list of strings)
Features used to build optional dependencies
(list of strings)
Features used to build optional dependencies
- rust-path
(list of strings, default [.])
Build specific workspace crates
Only one item is currently supported.
(list of strings, default [.])
Build specific crates inside the workspace
- rust-no-default-features
(boolean, default False)
Whether to disable the default features in this crate.
Equivalent to setting `--no-default-features` on the commandline.
- rust-use-global-lto
(boolean, default False)
Whether to use global LTO.
This option may significantly impact the build performance but
reducing the final binary size.
This will forcibly enable LTO for all the crates you specified,
regardless of whether you have LTO enabled in the Cargo.toml file
"""

import logging
import subprocess
from textwrap import dedent
from typing import Any, Dict, List, Set

from snapcraft_legacy.plugins.v2 import PluginV2

logger = logging.getLogger(__name__)


class RustPlugin(PluginV2):
@classmethod
Expand All @@ -51,8 +75,6 @@ def get_schema(cls) -> Dict[str, Any]:
"rust-path": {
"type": "array",
"minItems": 1,
# TODO support more than one item.
"maxItems": 1,
"uniqueItems": True,
"items": {"type": "string"},
"default": ["."],
Expand All @@ -63,6 +85,18 @@ def get_schema(cls) -> Dict[str, Any]:
"items": {"type": "string"},
"default": [],
},
"rust-channel": {
"type": ["string", "null"],
"default": None,
},
"rust-use-global-lto": {
"type": "boolean",
"default": False,
},
"rust-no-default-features": {
"type": "boolean",
"default": False,
},
},
"required": ["source"],
}
Expand All @@ -71,39 +105,95 @@ def get_build_snaps(self) -> Set[str]:
return set()

def get_build_packages(self) -> Set[str]:
return {"curl", "gcc", "git"}
return {"curl", "gcc", "git", "pkg-config", "findutils"}

def get_build_environment(self) -> Dict[str, str]:
return {"PATH": "${HOME}/.cargo/bin:${PATH}"}

def _get_rustup_command(self) -> str:
return dedent(
"""\
if ! command -v rustup 2>/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile=minimal
export PATH="${HOME}/.cargo/bin:${PATH}"
fi
"""
)

def _get_install_command(self) -> str:
cmd = [
"cargo",
"install",
"--locked",
"--path",
self.options.rust_path[0],
"--root",
'"${SNAPCRAFT_PART_INSTALL}"',
"--force",
def _check_system_rust(self) -> bool:
"""Check if Rust is installed on the system."""
try:
rust_version = subprocess.check_output(["rustc", "--version"], text=True)
cargo_version = subprocess.check_output(["cargo", "--version"], text=True)
return "rustc" in rust_version and "cargo" in cargo_version
except (subprocess.CalledProcessError, FileNotFoundError):
return False

def _check_rustup(self) -> bool:
try:
rustup_version = subprocess.check_output(["rustup", "--version"])
return "rustup" in rustup_version.decode("utf-8")
except (subprocess.CalledProcessError, FileNotFoundError):
return False

def _get_setup_rustup(self, channel: str) -> List[str]:
return [
f"""\
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --no-modify-path --profile=minimal --default-toolchain {channel}
"""
]

def _get_install_commands(self) -> List[str]:
"""Return a list of commands to run during the pull step."""
options = self.options
if not options.rust_channel and self._check_system_rust():
logger.info("Rust is installed on the system, skipping rustup")
return []

rust_channel = options.rust_channel or "stable"
if rust_channel == "none":
return []
if not self._check_rustup():
logger.info("Rustup not found, installing it")
return self._get_setup_rustup(rust_channel)
logger.info("Switch rustup channel to %s", rust_channel)
return [
f"rustup update {rust_channel}",
f"rustup default {rust_channel}",
]

if self.options.rust_features:
cmd.extend(
["--features", "'{}'".format(" ".join(self.options.rust_features))]
def get_build_commands(self) -> List[str]:
options = self.options

rust_build_cmd: List[str] = []
config_cmd: List[str] = []

if options.rust_features:
features_string = " ".join(options.rust_features)
config_cmd.extend(["--features", f"'{features_string}'"])

if options.rust_use_global_lto:
logger.info("Adding overrides for LTO support")
config_cmd.extend(
[
"--config 'profile.release.lto = true'",
"--config 'profile.release.codegen-units = 1'",
]
)

return " ".join(cmd)

def get_build_commands(self) -> List[str]:
return [self._get_rustup_command(), self._get_install_command()]
if options.rust_no_default_features:
config_cmd.append("--no-default-features")

for crate in options.rust_path:
logger.info("Generating build commands for %s", crate)
config_cmd_string = " ".join(config_cmd)
# pylint: disable=line-too-long
rust_build_cmd_single = dedent(
f"""\
if cargo read-manifest --manifest-path "{crate}"/Cargo.toml > /dev/null; then
cargo install -f --locked --path "{crate}" --root "${{SNAPCRAFT_PART_INSTALL}}" {config_cmd_string}
# remove the installation metadata
rm -f "${{SNAPCRAFT_PART_INSTALL}}"/.crates{{.toml,2.json}}
else
# virtual workspace is a bit tricky,
# we need to build the whole workspace and then copy the binaries ourselves
pushd "{crate}"
cargo build --workspace --release {config_cmd_string}
# install the final binaries
find ./target/release -maxdepth 1 -executable -exec install -Dvm755 {{}} "${{SNAPCRAFT_PART_INSTALL}}" ';'
popd
fi"""
)
rust_build_cmd.append(rust_build_cmd_single)
return self._get_install_commands() + rust_build_cmd
86 changes: 66 additions & 20 deletions tests/legacy/unit/plugins/v2/test_rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,33 +31,47 @@ def test_schema(self):
Equals(
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"additionalProperties": False,
"properties": {
"rust-features": {
"default": [],
"items": {"type": "string"},
"rust-path": {
"type": "array",
"minItems": 1,
"uniqueItems": True,
},
"rust-path": {
"default": ["."],
"items": {"type": "string"},
"maxItems": 1,
"minItems": 1,
"default": ["."],
},
"rust-features": {
"type": "array",
"uniqueItems": True,
"items": {"type": "string"},
"default": [],
},
"rust-channel": {
"type": ["string", "null"],
"default": None,
},
"rust-use-global-lto": {
"type": "boolean",
"default": False,
},
"rust-no-default-features": {
"type": "boolean",
"default": False,
},
},
"required": ["source"],
"type": "object",
}
),
)

def test_get_build_packages(self):
plugin = RustPlugin(part_name="my-part", options=lambda: None)

self.assertThat(plugin.get_build_packages(), Equals({"curl", "gcc", "git"}))
self.assertThat(
plugin.get_build_packages(),
Equals({"curl", "gcc", "git", "pkg-config", "findutils"}),
)

def test_get_build_environment(self):
plugin = RustPlugin(part_name="my-part", options=lambda: None)
Expand All @@ -69,40 +83,72 @@ def test_get_build_environment(self):

def test_get_build_commands(self):
class Options:
rust_channel = ""
rust_channel = "stable"
rust_path = ["."]
rust_features = []
rust_no_default_features = True
rust_use_global_lto = False

plugin = RustPlugin(part_name="my-part", options=Options())
plugin._check_rustup = lambda: True

self.assertThat(
plugin.get_build_commands(),
Equals(
[
"rustup update stable",
"rustup default stable",
dedent(
"""\
if ! command -v rustup 2>/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile=minimal
export PATH="${HOME}/.cargo/bin:${PATH}"
fi
"""
if cargo read-manifest --manifest-path "."/Cargo.toml > /dev/null; then
cargo install -f --locked --path "." --root "${SNAPCRAFT_PART_INSTALL}" --no-default-features
# remove the installation metadata
rm -f "${SNAPCRAFT_PART_INSTALL}"/.crates{.toml,2.json}
else
# virtual workspace is a bit tricky,
# we need to build the whole workspace and then copy the binaries ourselves
pushd "."
cargo build --workspace --release --no-default-features
# install the final binaries
find ./target/release -maxdepth 1 -executable -exec install -Dvm755 {} "${SNAPCRAFT_PART_INSTALL}" ';'
popd
fi"""
),
'cargo install --locked --path . --root "${SNAPCRAFT_PART_INSTALL}" --force',
]
),
)

def test_get_install_command_with_features(self):
class Options:
rust_channel = ""
rust_channel = "none"
rust_path = ["path"]
rust_features = ["my-feature", "your-feature"]
rust_no_default_features = False
rust_use_global_lto = False

plugin = RustPlugin(part_name="my-part", options=Options())
plugin._check_rustup = lambda: False

self.assertThat(
plugin._get_install_command(),
plugin.get_build_commands(),
Equals(
"cargo install --locked --path path --root \"${SNAPCRAFT_PART_INSTALL}\" --force --features 'my-feature your-feature'"
[
dedent(
"""\
if cargo read-manifest --manifest-path "path"/Cargo.toml > /dev/null; then
cargo install -f --locked --path "path" --root "${SNAPCRAFT_PART_INSTALL}" --features 'my-feature your-feature'
# remove the installation metadata
rm -f "${SNAPCRAFT_PART_INSTALL}"/.crates{.toml,2.json}
else
# virtual workspace is a bit tricky,
# we need to build the whole workspace and then copy the binaries ourselves
pushd "path"
cargo build --workspace --release --features 'my-feature your-feature'
# install the final binaries
find ./target/release -maxdepth 1 -executable -exec install -Dvm755 {} "${SNAPCRAFT_PART_INSTALL}" ';'
popd
fi"""
)
]
),
)

0 comments on commit 1ae07ec

Please sign in to comment.