cargo: Load Cargo.lock

Cargo.lock is essentially identical to subprojects/*.wrap files. When a
(sub)project has a Cargo.lock file this allows automatic fallback for
its cargo dependencies.
pull/13050/head
Xavier Claessens 9 months ago committed by Xavier Claessens
parent c0de2e1264
commit 9b8378985d
  1. 4
      docs/markdown/Wrap-dependency-system-manual.md
  2. 6
      docs/markdown/snippets/cargo_lock.md
  3. 5
      mesonbuild/cargo/__init__.py
  4. 46
      mesonbuild/cargo/interpreter.py
  5. 17
      mesonbuild/cargo/manifest.py
  6. 17
      mesonbuild/wrap/wrap.py
  7. 2
      run_unittests.py
  8. 7
      test cases/rust/25 cargo lock/Cargo.lock
  9. 3
      test cases/rust/25 cargo lock/meson.build
  10. BIN
      test cases/rust/25 cargo lock/subprojects/packagecache/bar-0.1.tar.gz
  11. 36
      unittests/cargotests.py

@ -377,6 +377,10 @@ Some naming conventions need to be respected:
- The `extra_deps` variable is pre-defined and can be used to add extra dependencies. - The `extra_deps` variable is pre-defined and can be used to add extra dependencies.
This is typically used as `extra_deps += dependency('foo')`. This is typically used as `extra_deps += dependency('foo')`.
Since *1.5.0* Cargo wraps can also be provided with `Cargo.lock` file at the root
of (sub)project source tree. Meson will automatically load that file and convert
it into a serie of wraps definitions.
## Using wrapped projects ## Using wrapped projects
Wraps provide a convenient way of obtaining a project into your Wraps provide a convenient way of obtaining a project into your

@ -0,0 +1,6 @@
## Added support `Cargo.lock` file
When a (sub)project has a `Cargo.lock` file at its root, it is loaded to provide
an automatic fallback for dependencies it defines, fetching code from
https://crates.io or git. This is identical as providing `subprojects/*.wrap`,
see [cargo wraps](Wrap-dependency-system-manual.md#cargo-wraps) dependency naming convention.

@ -1,5 +1,6 @@
__all__ = [ __all__ = [
'interpret' 'interpret',
'load_wraps',
] ]
from .interpreter import interpret from .interpreter import interpret, load_wraps

@ -18,12 +18,14 @@ import json
import os import os
import shutil import shutil
import collections import collections
import urllib.parse
import typing as T import typing as T
from . import builder from . import builder
from . import version from . import version
from ..mesonlib import MesonException, Popen_safe, OptionKey from ..mesonlib import MesonException, Popen_safe, OptionKey
from .. import coredata, options from .. import coredata, options, mlog
from ..wrap.wrap import PackageDefinition
if T.TYPE_CHECKING: if T.TYPE_CHECKING:
from types import ModuleType from types import ModuleType
@ -731,3 +733,45 @@ def interpret(subp_name: str, subdir: str, env: Environment) -> T.Tuple[mparser.
ast.extend(_create_lib(cargo, build, crate_type)) ast.extend(_create_lib(cargo, build, crate_type))
return build.block(ast), project_options return build.block(ast), project_options
def load_wraps(source_dir: str, subproject_dir: str) -> T.List[PackageDefinition]:
""" Convert Cargo.lock into a list of wraps """
wraps: T.List[PackageDefinition] = []
filename = os.path.join(source_dir, 'Cargo.lock')
if os.path.exists(filename):
cargolock = T.cast('manifest.CargoLock', load_toml(filename))
for package in cargolock['package']:
name = package['name']
version = package['version']
subp_name = _dependency_name(name, _version_to_api(version))
source = package.get('source')
if source is None:
# This is project's package, or one of its workspace members.
pass
elif source == 'registry+https://github.com/rust-lang/crates.io-index':
url = f'https://crates.io/api/v1/crates/{name}/{version}/download'
directory = f'{name}-{version}'
wraps.append(PackageDefinition.from_values(subp_name, subproject_dir, 'file', {
'directory': directory,
'source_url': url,
'source_filename': f'{directory}.tar.gz',
'source_hash': package['checksum'],
'method': 'cargo',
}))
elif source.startswith('git+'):
parts = urllib.parse.urlparse(source[4:])
query = urllib.parse.parse_qs(parts.query)
branch = query['branch'][0] if 'branch' in query else ''
revision = parts.fragment or branch
url = urllib.parse.urlunparse(parts._replace(params='', query='', fragment=''))
wraps.append(PackageDefinition.from_values(subp_name, subproject_dir, 'git', {
'directory': name,
'url': url,
'revision': revision,
'method': 'cargo',
}))
else:
mlog.warning(f'Unsupported source URL in {filename}: {source}')
return wraps

@ -225,3 +225,20 @@ class VirtualManifest(TypedDict):
""" """
workspace: Workspace workspace: Workspace
class CargoLockPackage(TypedDict, total=False):
"""A description of a package in the Cargo.lock file format."""
name: str
version: str
source: str
checksum: str
class CargoLock(TypedDict, total=False):
"""A description of the Cargo.lock file format."""
version: str
package: T.List[CargoLockPackage]

@ -324,25 +324,32 @@ class Resolver:
mlog.warning(f'failed to process netrc file: {e}.', fatal=False) mlog.warning(f'failed to process netrc file: {e}.', fatal=False)
def load_wraps(self) -> None: def load_wraps(self) -> None:
if not os.path.isdir(self.subdir_root): # Load Cargo.lock at the root of source tree
return source_dir = os.path.dirname(self.subdir_root)
if os.path.exists(os.path.join(source_dir, 'Cargo.lock')):
from .. import cargo
for wrap in cargo.load_wraps(source_dir, self.subdir_root):
self.wraps[wrap.name] = wrap
# Load subprojects/*.wrap
if os.path.isdir(self.subdir_root):
root, dirs, files = next(os.walk(self.subdir_root)) root, dirs, files = next(os.walk(self.subdir_root))
ignore_dirs = {'packagecache', 'packagefiles'}
for i in files: for i in files:
if not i.endswith('.wrap'): if not i.endswith('.wrap'):
continue continue
fname = os.path.join(self.subdir_root, i) fname = os.path.join(self.subdir_root, i)
wrap = PackageDefinition.from_wrap_file(fname, self.subproject) wrap = PackageDefinition.from_wrap_file(fname, self.subproject)
self.wraps[wrap.name] = wrap self.wraps[wrap.name] = wrap
ignore_dirs |= {wrap.directory, wrap.name}
# Add dummy package definition for directories not associated with a wrap file. # Add dummy package definition for directories not associated with a wrap file.
ignore_dirs = {'packagecache', 'packagefiles'}
for wrap in self.wraps.values():
ignore_dirs |= {wrap.directory, wrap.name}
for i in dirs: for i in dirs:
if i in ignore_dirs: if i in ignore_dirs:
continue continue
fname = os.path.join(self.subdir_root, i) fname = os.path.join(self.subdir_root, i)
wrap = PackageDefinition.from_directory(fname) wrap = PackageDefinition.from_directory(fname)
self.wraps[wrap.name] = wrap self.wraps[wrap.name] = wrap
# Add provided deps and programs into our lookup tables
for wrap in self.wraps.values(): for wrap in self.wraps.values():
self.add_wrap(wrap) self.add_wrap(wrap)

@ -25,7 +25,7 @@ from mesonbuild.mesonlib import python_command, setup_vsenv
import mesonbuild.modules.pkgconfig import mesonbuild.modules.pkgconfig
from unittests.allplatformstests import AllPlatformTests from unittests.allplatformstests import AllPlatformTests
from unittests.cargotests import CargoVersionTest, CargoCfgTest from unittests.cargotests import CargoVersionTest, CargoCfgTest, CargoLockTest
from unittests.darwintests import DarwinTests from unittests.darwintests import DarwinTests
from unittests.failuretests import FailureTests from unittests.failuretests import FailureTests
from unittests.linuxcrosstests import LinuxCrossArmTests, LinuxCrossMingwTests from unittests.linuxcrosstests import LinuxCrossArmTests, LinuxCrossMingwTests

@ -0,0 +1,7 @@
version = 3
[[package]]
name = "bar"
version = "0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2f34e570dcd5f9fe32e6863ee16ee73a356d3b77bce0d8c78501b8bc81a860"

@ -0,0 +1,3 @@
project('cargo lock')
dependency('bar-0.1-rs')

@ -3,9 +3,12 @@
from __future__ import annotations from __future__ import annotations
import unittest import unittest
import os
import tempfile
import textwrap
import typing as T import typing as T
from mesonbuild.cargo import builder, cfg from mesonbuild.cargo import builder, cfg, load_wraps
from mesonbuild.cargo.cfg import TokenType from mesonbuild.cargo.cfg import TokenType
from mesonbuild.cargo.version import convert from mesonbuild.cargo.version import convert
@ -185,3 +188,34 @@ class CargoCfgTest(unittest.TestCase):
with self.subTest(): with self.subTest():
value = cfg.ir_to_meson(cfg.parse(iter(cfg.lexer(data))), build) value = cfg.ir_to_meson(cfg.parse(iter(cfg.lexer(data))), build)
self.assertEqual(value, expected) self.assertEqual(value, expected)
class CargoLockTest(unittest.TestCase):
def test_cargo_lock(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
with open(os.path.join(tmpdir, 'Cargo.lock'), 'w', encoding='utf-8') as f:
f.write(textwrap.dedent('''\
version = 3
[[package]]
name = "foo"
version = "0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
[[package]]
name = "bar"
version = "0.1"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=0.19#23c5599424cc75ec66618891c915d9f490f6e4c2"
'''))
wraps = load_wraps(tmpdir, 'subprojects')
self.assertEqual(len(wraps), 2)
self.assertEqual(wraps[0].name, 'foo-0.1-rs')
self.assertEqual(wraps[0].directory, 'foo-0.1')
self.assertEqual(wraps[0].type, 'file')
self.assertEqual(wraps[0].get('method'), 'cargo')
self.assertEqual(wraps[0].get('source_url'), 'https://crates.io/api/v1/crates/foo/0.1/download')
self.assertEqual(wraps[0].get('source_hash'), '8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb')
self.assertEqual(wraps[1].name, 'bar-0.1-rs')
self.assertEqual(wraps[1].directory, 'bar')
self.assertEqual(wraps[1].type, 'git')
self.assertEqual(wraps[1].get('method'), 'cargo')
self.assertEqual(wraps[1].get('url'), 'https://github.com/gtk-rs/gtk-rs-core')
self.assertEqual(wraps[1].get('revision'), '23c5599424cc75ec66618891c915d9f490f6e4c2')

Loading…
Cancel
Save