diff --git a/.bzrignore b/.bzrignore index f7c1520587c..cc84f1febdc 100644 --- a/.bzrignore +++ b/.bzrignore @@ -15,3 +15,5 @@ dist htmlcov __pycache__ docs/**.html +Cargo.lock +target diff --git a/integration_tests/snaps/simple-rust/Cargo.toml b/integration_tests/snaps/simple-rust/Cargo.toml new file mode 100644 index 00000000000..47a0d58ea09 --- /dev/null +++ b/integration_tests/snaps/simple-rust/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "simple-rust" +version = "0.1.0" +authors = ["Marius Gripsgard "] + +[dependencies] diff --git a/integration_tests/snaps/simple-rust/snapcraft.yaml b/integration_tests/snaps/simple-rust/snapcraft.yaml new file mode 100644 index 00000000000..a263c9dc1f2 --- /dev/null +++ b/integration_tests/snaps/simple-rust/snapcraft.yaml @@ -0,0 +1,10 @@ +name: test-package +version: 0.1 +summary: A simple rust project. +description: A simple rust project. +confinement: strict + +parts: + simple-rust: + plugin: rust + source: . diff --git a/integration_tests/snaps/simple-rust/src/main.rs b/integration_tests/snaps/simple-rust/src/main.rs new file mode 100644 index 00000000000..17779d6ccd7 --- /dev/null +++ b/integration_tests/snaps/simple-rust/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("There is rust on snaps!"); +} diff --git a/integration_tests/test_rust_plugin.py b/integration_tests/test_rust_plugin.py new file mode 100644 index 00000000000..ebb9ff63eb6 --- /dev/null +++ b/integration_tests/test_rust_plugin.py @@ -0,0 +1,25 @@ +# Copyright (C) 2016 Marius Gripsgard (mariogrip@ubuntu.com) +# +# 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 +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os, integration_tests + +class RustPluginTestCase(integration_tests.TestCase): + + def test_stage_rust_plugin(self): + project_dir = 'simple-rust' + self.run_snapcraft('stage', project_dir) + + binary_output = self.get_output_ignoring_non_zero_exit( + os.path.join('stage', 'bin', 'simple-rust'), cwd=project_dir) + self.assertEqual("There is rust on snaps!\n", binary_output) diff --git a/snapcraft/plugins/rust.py b/snapcraft/plugins/rust.py new file mode 100644 index 00000000000..7a4341c5c7c --- /dev/null +++ b/snapcraft/plugins/rust.py @@ -0,0 +1,102 @@ +# Copyright (C) 2016 Marius Gripsgard (mariogrip@ubuntu.com) +# +# 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 +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""This rust plugin is useful for building rust based parts. + +Rust uses cargo to drive the build. + +This plugin uses the common plugin keywords as well as those for "sources". +For more information check the 'plugins' topic for the former and the +'sources' topic for the latter. + +Additionally, this plugin uses the following plugin-specific keywords: + + - rust-channel + (string) + select rust channel (stable, beta, nightly) + - rust-revision + (string) + select rust version +""" + +import os +import snapcraft +import shutil + + +class RustPlugin(snapcraft.BasePlugin): + + @classmethod + def schema(cls): + schema = super().schema() + schema['properties']['rust-channel'] = { + 'type': 'string', + } + schema['properties']['rust-revision'] = { + 'type': 'string', + } + return schema + + def __init__(self, name, options, project): + super().__init__(name, options, project) + self._rustpath = os.path.join(self.partdir, "rust") + self._rustc = os.path.join(self._rustpath, "bin", "rustc") + self._rustdoc = os.path.join(self._rustpath, "bin", "rustdoc") + self._cargo = os.path.join(self._rustpath, "bin", "cargo") + self._rustlib = os.path.join(self._rustpath, "lib") + + def build(self): + super().build() + self.run([self._cargo, "install", + "-j{}".format(self.project.parallel_build_count), + "--root", self.installdir], env=self._build_env()) + + def _build_env(self): + env = os.environ.copy() + env.update({"RUSTC": self._rustc, + "RUSTDOC": self._rustdoc, + "RUST_PATH": self._rustlib}) + return env + + def pull(self): + super().pull() + self._fetch_rust() + + def clean_pull(self): + super().clean_pull() + + # Remove the rust path (if any) + if os.path.exists(self._rustpath): + shutil.rmtree(self._rustpath) + + def _fetch_rust(self): + options = [] + + if self.options.rust_revision: + options.append("--revision=%s" % self.options.rust_revision) + + if self.options.rust_channel: + if self.options.rust_channel in ["stable", "beta", "nightly"]: + options.append("--channel=%s" % self.options.rust_channel) + else: + raise EnvironmentError("%s is not a valid rust channel" + % self.options.rust_channel) + + rustup = "rustup.sh" + self.run(["curl", "https://static.rust-lang.org/rustup.sh", + "-o", rustup]) + self.run(["chmod", "+x", rustup]) + self.run(["./%s" % rustup, + "--prefix=%s" % self._rustpath, + "--disable-sudo", "--save"]) diff --git a/snapcraft/tests/test_commands_list_plugins.py b/snapcraft/tests/test_commands_list_plugins.py index 306836cf196..98bd3207c45 100644 --- a/snapcraft/tests/test_commands_list_plugins.py +++ b/snapcraft/tests/test_commands_list_plugins.py @@ -27,9 +27,9 @@ class ListPluginsCommandTestCase(tests.TestCase): # plugin list when wrapper at MAX_CHARACTERS_WRAP default_plugin_output = ( 'ant catkin copy gulp kbuild make nil python2 ' - 'qmake tar-content\n' + 'qmake scons \n' 'autotools cmake go jdk kernel maven nodejs python3 ' - 'scons\n') + 'rust tar-content\n') @mock.patch('sys.stdout', new_callable=io.StringIO) @mock.patch('subprocess.check_output') @@ -44,9 +44,9 @@ def test_list_plugins_small_terminal(self, mock_subprocess, mock_stdout): mock_subprocess.return_value = "60" expected_output = ( 'ant copy kbuild nil qmake \n' - 'autotools go kernel nodejs scons \n' - 'catkin gulp make python2 tar-content\n' - 'cmake jdk maven python3\n') + 'autotools go kernel nodejs rust \n' + 'catkin gulp make python2 scons \n' + 'cmake jdk maven python3 tar-content\n') main(['list-plugins']) self.assertEqual(mock_stdout.getvalue(), expected_output) diff --git a/snapcraft/tests/test_plugin_rust.py b/snapcraft/tests/test_plugin_rust.py new file mode 100644 index 00000000000..837665f337a --- /dev/null +++ b/snapcraft/tests/test_plugin_rust.py @@ -0,0 +1,97 @@ +# Copyright (C) 2016 Marius Gripsgard (mariogrip@ubuntu.com) +# +# 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 +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from unittest import mock + +import snapcraft +from snapcraft import tests +from snapcraft.plugins import rust + + +class RustPluginTestCase(tests.TestCase): + + def setUp(self): + super().setUp() + + class Options: + makefile = None + make_parameters = [] + + self.options = Options() + self.project_options = snapcraft.ProjectOptions() + + def test_schema(self): + schema = rust.RustPlugin.schema() + + properties = schema['properties'] + self.assertTrue('rust-channel' in properties, + 'Expected "rust-channel" to be included in properties') + self.assertTrue('rust-revision' in properties, + 'Expected "rust-revision to be included in properties') + + rust_channel = properties['rust-channel'] + self.assertTrue('type' in rust_channel, + 'Expected "type" to be included in "rust-channel"') + + rust_channel_type = rust_channel['type'] + self.assertEqual(rust_channel_type, 'string', + 'Expected "rust-channel" "type" to be "string", ' + 'but it was "{}"'.format(rust_channel_type)) + + rust_revision = properties['rust-revision'] + self.assertTrue('type' in rust_revision, + 'Expected "type" to be included in "rust-revision"') + + rust_revision_type = rust_revision['type'] + self.assertEqual(rust_revision_type, 'string', + 'Expected "rust-revision" "type" to be "string", ' + 'but it was "{}"'.format(rust_revision_type)) + + @mock.patch.object(rust.RustPlugin, 'run') + def test_build(self, run_mock): + plugin = rust.RustPlugin('test-part', self.options, + self.project_options) + os.makedirs(plugin.sourcedir) + + plugin.build() + + self.assertEqual(1, run_mock.call_count) + run_mock.assert_has_calls([ + mock.call([plugin._cargo, 'install', + '-j{}'.format(plugin.project.parallel_build_count), + '--root', plugin.installdir], env=plugin._build_env()) + ]) + + @mock.patch.object(rust.RustPlugin, 'run') + def test_pull(self, run_mock): + plugin = rust.RustPlugin('test-part', self.options, + self.project_options) + os.makedirs(plugin.sourcedir) + plugin.options.rust_revision = [] + plugin.options.rust_channel = [] + + plugin.pull() + rustup = "rustup.sh" + + self.assertEqual(3, run_mock.call_count) + run_mock.assert_has_calls([ + mock.call(["curl", "https://static.rust-lang.org/rustup.sh", + "-o", rustup]), + mock.call(["chmod", "+x", rustup]), + mock.call(["./%s" % rustup, + "--prefix=%s" % plugin._rustpath, + "--disable-sudo", "--save"]) + ])