Skip to content

Commit

Permalink
cargo: Load Cargo.lock
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
xclaesse committed Mar 15, 2024
1 parent f3972fe commit af65b2c
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 24 deletions.
4 changes: 4 additions & 0 deletions docs/markdown/Wrap-dependency-system-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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

Wraps provide a convenient way of obtaining a project into your
Expand Down
6 changes: 6 additions & 0 deletions docs/markdown/snippets/cargo_lock.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 3 additions & 2 deletions mesonbuild/cargo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
__all__ = [
'interpret'
'interpret',
'load_wraps',
]

from .interpreter import interpret
from .interpreter import interpret, load_wraps
46 changes: 45 additions & 1 deletion mesonbuild/cargo/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
import os
import shutil
import collections
import urllib.parse
import typing as T

from . import builder
from . import version
from ..mesonlib import MesonException, Popen_safe, OptionKey
from .. import coredata
from .. import coredata, mlog
from ..wrap.wrap import PackageDefinition

if T.TYPE_CHECKING:
from types import ModuleType
Expand Down Expand Up @@ -731,3 +733,45 @@ def interpret(subp_name: str, subdir: str, env: Environment) -> T.Tuple[mparser.
ast.extend(_create_lib(cargo, build, crate_type))

return build.block(ast), 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
17 changes: 17 additions & 0 deletions mesonbuild/cargo/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,20 @@ class VirtualManifest(TypedDict):
"""

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]
45 changes: 26 additions & 19 deletions mesonbuild/wrap/wrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,25 +324,32 @@ def load_netrc(self) -> None:
mlog.warning(f'failed to process netrc file: {e}.', fatal=False)

def load_wraps(self) -> None:
if not os.path.isdir(self.subdir_root):
return
root, dirs, files = next(os.walk(self.subdir_root))
ignore_dirs = {'packagecache', 'packagefiles'}
for i in files:
if not i.endswith('.wrap'):
continue
fname = os.path.join(self.subdir_root, i)
wrap = PackageDefinition.from_wrap_file(fname, self.subproject)
self.wraps[wrap.name] = wrap
ignore_dirs |= {wrap.directory, wrap.name}
# Add dummy package definition for directories not associated with a wrap file.
for i in dirs:
if i in ignore_dirs:
continue
fname = os.path.join(self.subdir_root, i)
wrap = PackageDefinition.from_directory(fname)
self.wraps[wrap.name] = wrap

# Load Cargo.lock at the root of source tree
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))
for i in files:
if not i.endswith('.wrap'):
continue
fname = os.path.join(self.subdir_root, i)
wrap = PackageDefinition.from_wrap_file(fname, self.subproject)
self.wraps[wrap.name] = wrap
# 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:
if i in ignore_dirs:
continue
fname = os.path.join(self.subdir_root, i)
wrap = PackageDefinition.from_directory(fname)
self.wraps[wrap.name] = wrap
# Add provided deps and programs into our lookup tables
for wrap in self.wraps.values():
self.add_wrap(wrap)

Expand Down
2 changes: 1 addition & 1 deletion run_unittests.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import mesonbuild.modules.pkgconfig

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.failuretests import FailureTests
from unittests.linuxcrosstests import LinuxCrossArmTests, LinuxCrossMingwTests
Expand Down
7 changes: 7 additions & 0 deletions test cases/rust/25 cargo lock/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions test cases/rust/25 cargo lock/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
project('cargo lock')

dependency('bar-0.1-rs')
Binary file not shown.
36 changes: 35 additions & 1 deletion unittests/cargotests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

from __future__ import annotations
import unittest
import os
import tempfile
import textwrap
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.version import convert

Expand Down Expand Up @@ -185,3 +188,34 @@ def test_ir_to_meson(self) -> None:
with self.subTest():
value = cfg.ir_to_meson(cfg.parse(iter(cfg.lexer(data))), build)
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')

0 comments on commit af65b2c

Please sign in to comment.