Valentin David pushed to branch valentindavid/cargo_plugin at BuildStream / buildstream
Commits:
-
516e990e
by ctolentino8 at 2018-10-31T11:36:46Z
-
b8a37a63
by Tristan Van Berkom at 2018-11-01T10:16:25Z
-
b27b592a
by Benjamin Schubert at 2018-11-01T10:49:57Z
-
89ace5d7
by Benjamin Schubert at 2018-11-01T11:16:36Z
-
d86daded
by Valentin David at 2018-11-01T11:41:10Z
5 changed files:
- buildstream/_frontend/app.py
- + buildstream/plugins/sources/cargo.py
- doc/source/core_plugins.rst
- setup.py
- tests/frontend/init.py
Changes:
| ... | ... | @@ -305,7 +305,6 @@ class App(): |
| 305 | 305 |
directory = self._main_options['directory']
|
| 306 | 306 |
directory = os.path.abspath(directory)
|
| 307 | 307 |
project_path = os.path.join(directory, 'project.conf')
|
| 308 |
- elements_path = os.path.join(directory, element_path)
|
|
| 309 | 308 |
|
| 310 | 309 |
try:
|
| 311 | 310 |
# Abort if the project.conf already exists, unless `--force` was specified in `bst init`
|
| ... | ... | @@ -335,6 +334,7 @@ class App(): |
| 335 | 334 |
raise AppError("Error creating project directory {}: {}".format(directory, e)) from e
|
| 336 | 335 |
|
| 337 | 336 |
# Create the elements sub-directory if it doesnt exist
|
| 337 |
+ elements_path = os.path.join(directory, element_path)
|
|
| 338 | 338 |
try:
|
| 339 | 339 |
os.makedirs(elements_path, exist_ok=True)
|
| 340 | 340 |
except IOError as e:
|
| 1 |
+#
|
|
| 2 |
+# Copyright (C) 2018 Codethink Limited
|
|
| 3 |
+#
|
|
| 4 |
+# This program is free software; you can redistribute it and/or
|
|
| 5 |
+# modify it under the terms of the GNU Lesser General Public
|
|
| 6 |
+# License as published by the Free Software Foundation; either
|
|
| 7 |
+# version 2 of the License, or (at your option) any later version.
|
|
| 8 |
+#
|
|
| 9 |
+# This library is distributed in the hope that it will be useful,
|
|
| 10 |
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 11 |
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
| 12 |
+# Lesser General Public License for more details.
|
|
| 13 |
+#
|
|
| 14 |
+# You should have received a copy of the GNU Lesser General Public
|
|
| 15 |
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
| 16 |
+#
|
|
| 17 |
+# Authors:
|
|
| 18 |
+# Valentin David <valentin david codethink co uk>
|
|
| 19 |
+ |
|
| 20 |
+"""
|
|
| 21 |
+cargo - stage files from cargo manifest
|
|
| 22 |
+=======================================
|
|
| 23 |
+ |
|
| 24 |
+`cargo` downloads and stages cargo crates based on a `Cargo.toml`
|
|
| 25 |
+manifest provided by a previous source.
|
|
| 26 |
+ |
|
| 27 |
+`ref` will contain the `Cargo.lock` file. `bst track` should be used
|
|
| 28 |
+to set it.
|
|
| 29 |
+ |
|
| 30 |
+When `keep-lock` is true, tracking will store the current `Cargo.lock`
|
|
| 31 |
+provided by previous sources. in the `ref`. If `keep-lock` is false or
|
|
| 32 |
+absent, then `ref` will be created for the latest available crates.
|
|
| 33 |
+ |
|
| 34 |
+**Host dependencies:**
|
|
| 35 |
+ |
|
| 36 |
+ * cargo
|
|
| 37 |
+ * cargo-vendor (can be installed with `cargo install cargo-vendor`).
|
|
| 38 |
+ |
|
| 39 |
+**Usage:**
|
|
| 40 |
+ |
|
| 41 |
+.. code:: yaml
|
|
| 42 |
+ |
|
| 43 |
+ # Specify the cargo source kind
|
|
| 44 |
+ kind: cargo
|
|
| 45 |
+ |
|
| 46 |
+ # Optionally give the subdirectory where the `Cargo.toml` manifest
|
|
| 47 |
+ # can be found.
|
|
| 48 |
+ subdir: subproject
|
|
| 49 |
+ |
|
| 50 |
+ # Optionally disallow rewriting `Cargo.lock`. In this case tracking
|
|
| 51 |
+ # will just read the existing file. If not used, then tracking
|
|
| 52 |
+ # will create `Cargo.lock`.
|
|
| 53 |
+ keep-lock: true
|
|
| 54 |
+"""
|
|
| 55 |
+ |
|
| 56 |
+ |
|
| 57 |
+import hashlib
|
|
| 58 |
+import os
|
|
| 59 |
+import errno
|
|
| 60 |
+ |
|
| 61 |
+from buildstream import Consistency, Source, utils, SourceError
|
|
| 62 |
+ |
|
| 63 |
+ |
|
| 64 |
+class CargoSource(Source):
|
|
| 65 |
+ # pylint: disable=attribute-defined-outside-init
|
|
| 66 |
+ |
|
| 67 |
+ BST_REQUIRES_PREVIOUS_SOURCES_TRACK = True
|
|
| 68 |
+ BST_REQUIRES_PREVIOUS_SOURCES_FETCH = True
|
|
| 69 |
+ |
|
| 70 |
+ def configure(self, node):
|
|
| 71 |
+ self.node_validate(node, ['ref', 'subdir', 'keep-lock'] + Source.COMMON_CONFIG_KEYS)
|
|
| 72 |
+ self.ref = self.node_get_member(node, str, 'ref', None)
|
|
| 73 |
+ self.subdir = self.node_get_member(node, str, 'subdir', '.')
|
|
| 74 |
+ self.keeplock = self.node_get_member(node, bool, 'keep-lock', False)
|
|
| 75 |
+ self.extra_path = None
|
|
| 76 |
+ |
|
| 77 |
+ def preflight(self):
|
|
| 78 |
+ self.host_cargo = utils.get_host_tool('cargo')
|
|
| 79 |
+ |
|
| 80 |
+ try:
|
|
| 81 |
+ utils.get_host_tool('cargo-vendor')
|
|
| 82 |
+ except utils.ProgramNotFoundError:
|
|
| 83 |
+ cargo_home = os.environ.get('CARGO_HOME', os.path.expanduser('~/.cargo'))
|
|
| 84 |
+ self.extra_path = os.path.join(cargo_home, 'bin')
|
|
| 85 |
+ |
|
| 86 |
+ self.call([self.host_cargo, 'vendor', '-V'],
|
|
| 87 |
+ env=self._environment(),
|
|
| 88 |
+ fail='Cannot find "cargo vendor". Please install it with "cargo install cargo-vendor".')
|
|
| 89 |
+ |
|
| 90 |
+ def get_unique_key(self):
|
|
| 91 |
+ return [self.subdir, self.ref]
|
|
| 92 |
+ |
|
| 93 |
+ def get_ref(self):
|
|
| 94 |
+ return self.ref
|
|
| 95 |
+ |
|
| 96 |
+ def load_ref(self, node):
|
|
| 97 |
+ self.ref = self.node_get_member(node, str, 'ref', None)
|
|
| 98 |
+ |
|
| 99 |
+ def set_ref(self, ref, node):
|
|
| 100 |
+ node['ref'] = self.ref = ref
|
|
| 101 |
+ |
|
| 102 |
+ def _environment(self, *, set_home=False):
|
|
| 103 |
+ env = {}
|
|
| 104 |
+ env.update(os.environ)
|
|
| 105 |
+ if self.extra_path:
|
|
| 106 |
+ path = env.get('PATH', '').split(':')
|
|
| 107 |
+ path.append(self.extra_path)
|
|
| 108 |
+ env['PATH'] = ':'.join(path)
|
|
| 109 |
+ if set_home:
|
|
| 110 |
+ home = os.path.join(self.get_mirror_directory(), 'home')
|
|
| 111 |
+ os.makedirs(home, exist_ok=True)
|
|
| 112 |
+ env['CARGO_HOME'] = home
|
|
| 113 |
+ return env
|
|
| 114 |
+ |
|
| 115 |
+ def _get_manifest(self, directory):
|
|
| 116 |
+ projectdir = os.path.join(directory, self.subdir)
|
|
| 117 |
+ manifest = os.path.join(projectdir, 'Cargo.toml')
|
|
| 118 |
+ lockfile = os.path.join(projectdir, 'Cargo.lock')
|
|
| 119 |
+ return manifest, lockfile
|
|
| 120 |
+ |
|
| 121 |
+ def track(self, previous_sources_dir):
|
|
| 122 |
+ manifest, lockfile = self._get_manifest(previous_sources_dir)
|
|
| 123 |
+ |
|
| 124 |
+ if not self.keeplock:
|
|
| 125 |
+ self.call([self.host_cargo, 'generate-lockfile', '--manifest-path', manifest],
|
|
| 126 |
+ env=self._environment(set_home=True),
|
|
| 127 |
+ fail="Failed to track cargo packages")
|
|
| 128 |
+ try:
|
|
| 129 |
+ with open(lockfile, 'rb') as f:
|
|
| 130 |
+ lockcontent = f.read().decode('utf-8')
|
|
| 131 |
+ except OSError as e:
|
|
| 132 |
+ if self.keeplock and e.errno == errno.ENOENT:
|
|
| 133 |
+ raise SourceError("{}: Cannot find Cargo.lock".format(self))
|
|
| 134 |
+ else:
|
|
| 135 |
+ raise
|
|
| 136 |
+ |
|
| 137 |
+ return lockcontent
|
|
| 138 |
+ |
|
| 139 |
+ def _get_stamp(self):
|
|
| 140 |
+ h = hashlib.sha256()
|
|
| 141 |
+ h.update(self.get_ref().encode('utf-8'))
|
|
| 142 |
+ return os.path.join(self.get_mirror_directory(), 'stamps', h.hexdigest())
|
|
| 143 |
+ |
|
| 144 |
+ def get_consistency(self):
|
|
| 145 |
+ if not self.ref:
|
|
| 146 |
+ return Consistency.INCONSISTENT
|
|
| 147 |
+ if os.path.exists(self._get_stamp()):
|
|
| 148 |
+ return Consistency.CACHED
|
|
| 149 |
+ return Consistency.RESOLVED
|
|
| 150 |
+ |
|
| 151 |
+ def fetch(self, previous_sources_dir):
|
|
| 152 |
+ manifest, lockfile = self._get_manifest(previous_sources_dir)
|
|
| 153 |
+ if not self.keeplock:
|
|
| 154 |
+ with open(lockfile, 'wb') as f:
|
|
| 155 |
+ f.write(self.get_ref().encode('utf-8'))
|
|
| 156 |
+ |
|
| 157 |
+ self.call([self.host_cargo, 'fetch', '--manifest-path', manifest, '--locked'],
|
|
| 158 |
+ env=self._environment(set_home=True),
|
|
| 159 |
+ fail="Failed to fetch cargo packages")
|
|
| 160 |
+ stamp = self._get_stamp()
|
|
| 161 |
+ os.makedirs(os.path.dirname(stamp), exist_ok=True)
|
|
| 162 |
+ with open(stamp, 'w'):
|
|
| 163 |
+ pass
|
|
| 164 |
+ |
|
| 165 |
+ def stage(self, directory):
|
|
| 166 |
+ manifest, lockfile = self._get_manifest(directory)
|
|
| 167 |
+ if not self.keeplock:
|
|
| 168 |
+ with open(lockfile, 'wb') as f:
|
|
| 169 |
+ f.write(self.ref.encode('utf-8'))
|
|
| 170 |
+ |
|
| 171 |
+ config = os.path.join(os.path.dirname(manifest), '.cargo', 'config')
|
|
| 172 |
+ os.makedirs(os.path.dirname(config), exist_ok=True)
|
|
| 173 |
+ |
|
| 174 |
+ vendordir = os.path.join(directory, 'vendor')
|
|
| 175 |
+ relvendordir = os.path.relpath(vendordir, os.path.dirname(manifest))
|
|
| 176 |
+ |
|
| 177 |
+ with utils.save_file_atomic(config, 'wb') as f:
|
|
| 178 |
+ self.call([self.host_cargo, 'vendor', '--frozen', '--relative-path', relvendordir],
|
|
| 179 |
+ env=self._environment(set_home=True),
|
|
| 180 |
+ cwd=os.path.dirname(manifest),
|
|
| 181 |
+ stdout=f,
|
|
| 182 |
+ fail="Failed to stage cargo packages")
|
|
| 183 |
+ |
|
| 184 |
+ |
|
| 185 |
+def setup():
|
|
| 186 |
+ return CargoSource
|
| ... | ... | @@ -59,6 +59,7 @@ Sources |
| 59 | 59 |
sources/patch
|
| 60 | 60 |
sources/deb
|
| 61 | 61 |
sources/pip
|
| 62 |
+ sources/cargo
|
|
| 62 | 63 |
|
| 63 | 64 |
|
| 64 | 65 |
External plugins
|
| ... | ... | @@ -39,6 +39,7 @@ if sys.version_info[0] != REQUIRED_PYTHON_MAJOR or sys.version_info[1] < REQUIRE |
| 39 | 39 |
try:
|
| 40 | 40 |
from setuptools import setup, find_packages, Command
|
| 41 | 41 |
from setuptools.command.easy_install import ScriptWriter
|
| 42 |
+ from setuptools.command.test import test as TestCommand
|
|
| 42 | 43 |
except ImportError:
|
| 43 | 44 |
print("BuildStream requires setuptools in order to build. Install it using"
|
| 44 | 45 |
" your package manager (usually python3-setuptools) or via pip (pip3"
|
| ... | ... | @@ -219,9 +220,48 @@ class BuildGRPC(Command): |
| 219 | 220 |
f.write(code)
|
| 220 | 221 |
|
| 221 | 222 |
|
| 223 |
+#####################################################
|
|
| 224 |
+# Pytest command #
|
|
| 225 |
+#####################################################
|
|
| 226 |
+class PyTest(TestCommand):
|
|
| 227 |
+ """Defines a pytest command class to run tests from setup.py"""
|
|
| 228 |
+ |
|
| 229 |
+ user_options = TestCommand.user_options + [
|
|
| 230 |
+ ("addopts=", None, "Arguments to pass to pytest"),
|
|
| 231 |
+ ('index-url=''build_grpc': BuildGRPC,
|
|
| 264 |
+ 'pytest': PyTest,
|
|
| 225 | 265 |
}
|
| 226 | 266 |
cmdclass.update(versioneer.get_cmdclass())
|
| 227 | 267 |
return cmdclass
|
| ... | ... | @@ -305,6 +345,5 @@ setup(name='BuildStream', |
| 305 | 345 |
'grpcio >= 1.10',
|
| 306 | 346 |
],
|
| 307 | 347 |
entry_points=bst_install_entry_points,
|
| 308 |
- setup_requires=['pytest-runner'],
|
|
| 309 | 348 |
tests_require=dev_requires,
|
| 310 | 349 |
zip_safe=False)
|
| ... | ... | @@ -3,6 +3,7 @@ import pytest |
| 3 | 3 |
from tests.testutils import cli
|
| 4 | 4 |
|
| 5 | 5 |
from buildstream import _yaml
|
| 6 |
+from buildstream._frontend.app import App
|
|
| 6 | 7 |
from buildstream._exceptions import ErrorDomain, LoadErrorReason
|
| 7 | 8 |
from buildstream._versions import BST_FORMAT_VERSION
|
| 8 | 9 |
|
| ... | ... | @@ -98,3 +99,34 @@ def test_bad_element_path(cli, tmpdir, element_path): |
| 98 | 99 |
'init', '--project-name', 'foo', '--element-path', element_path
|
| 99 | 100 |
])
|
| 100 | 101 |
result.assert_main_error(ErrorDomain.APP, 'invalid-element-path')
|
| 102 |
+ |
|
| 103 |
+ |
|
| 104 |
+@pytest.mark.parametrize("element_path", [('foo'), ('foo/bar')])
|
|
| 105 |
+def test_element_path_interactive(cli, tmp_path, monkeypatch, element_path):
|
|
| 106 |
+ project = tmp_path
|
|
| 107 |
+ project_conf_path = project.joinpath('project.conf')
|
|
| 108 |
+ |
|
| 109 |
+ class DummyInteractiveApp(App):
|
|
| 110 |
+ def __init__(self, *args, **kwargs):
|
|
| 111 |
+ super().__init__(*args, **kwargs)
|
|
| 112 |
+ self.interactive = True
|
|
| 113 |
+ |
|
| 114 |
+ @classmethod
|
|
| 115 |
+ def create(cls, *args, **kwargs):
|
|
| 116 |
+ return DummyInteractiveApp(*args, **kwargs)
|
|
| 117 |
+ |
|
| 118 |
+ def _init_project_interactive(self, *args, **kwargs):
|
|
| 119 |
+ return ('project_name', '0', element_path)
|
|
| 120 |
+ |
|
| 121 |
+ monkeypatch.setattr(App, 'create', DummyInteractiveApp.create)
|
|
| 122 |
+ |
|
| 123 |
+ result = cli.run(project=str(project), args=['init'])
|
|
| 124 |
+ result.assert_success()
|
|
| 125 |
+ |
|
| 126 |
+ full_element_path = project.joinpath(element_path)
|
|
| 127 |
+ assert full_element_path.exists()
|
|
| 128 |
+ |
|
| 129 |
+ project_conf = _yaml.load(str(project_conf_path))
|
|
| 130 |
+ assert project_conf['name'] == 'project_name'
|
|
| 131 |
+ assert project_conf['format-version'] == '0'
|
|
| 132 |
+ assert project_conf['element-path'] == element_path
|
