File: update-rust-deps

package info (click to toggle)
debmutate 0.80
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 588 kB
  • sloc: python: 8,346; makefile: 12; sh: 1
file content (334 lines) | stat: -rwxr-xr-x 11,775 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
#!/usr/bin/env python3
"""Update debian/control build dependencies from debcargo output."""

import argparse
import json
import logging
import subprocess
import sys
from pathlib import Path
from typing import List, Set, Tuple

from debian.deb822 import PkgRelation

from debmutate.control import ControlEditor, PkgRelationFieldEditor, ensure_relation
from debmutate.debcargo import debcargo_binary_name


def discover_workspace_crates() -> Tuple[List[Path], Set[str]]:
    """Discover all Cargo.toml files for workspace crates.

    Uses `cargo metadata` to find all workspace member crates.

    Returns:
        Tuple of:
            - List of paths to Cargo.toml files for workspace crates
            - Set of crate names in the workspace

    Raises:
        subprocess.CalledProcessError: If cargo metadata command fails
    """
    result = subprocess.run(
        ["cargo", "metadata", "--no-deps", "--format-version", "1"],
        capture_output=True,
        text=True,
        check=True,
    )

    metadata = json.loads(result.stdout)

    # Get workspace member IDs
    workspace_member_ids = set(metadata["workspace_members"])

    # Find the manifest paths and crate names for workspace members
    manifest_paths = []
    crate_names = set()
    for package in metadata["packages"]:
        if package["id"] in workspace_member_ids:
            manifest_path = Path(package["manifest_path"])
            manifest_paths.append(manifest_path)
            crate_names.add(package["name"])

    return manifest_paths, crate_names


def get_debcargo_dependencies(
    cargo_toml_path: Path,
    features: List[str] | None = None,
    all_features: bool = False,
    no_default_features: bool = False,
    allow_prerelease_deps: bool = False,
    include_dev_dependencies: bool = False,
) -> List[str]:
    """Run debcargo deb-dependencies and parse the output.

    Args:
        cargo_toml_path: Path to the Cargo.toml file
        features: Features to include in dependencies
        all_features: Include all features in dependencies
        no_default_features: Do not include default feature in dependencies
        allow_prerelease_deps: Allow prerelease versions of dependencies
        include_dev_dependencies: Include dev dependencies

    Returns:
        List of dependency strings

    Raises:
        subprocess.CalledProcessError: If debcargo command fails
    """
    cmd = ["debcargo", "deb-dependencies"]

    if features:
        for feature in features:
            cmd.extend(["--features", feature])
    if all_features:
        cmd.append("--all-features")
    if no_default_features:
        cmd.append("--no-default-features")
    if allow_prerelease_deps:
        cmd.append("--allow-prerelease-deps")
    if include_dev_dependencies:
        cmd.append("--include-dev-dependencies")

    cmd.append(str(cargo_toml_path))

    result = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        check=True,
    )

    # The output is comma-separated dependencies
    deps = [dep.strip() for dep in result.stdout.strip().split(",")]
    return [dep for dep in deps if dep]


def filter_local_crate_dependencies(
    deps: List[str], local_crate_names: Set[str]
) -> List[str]:
    """Filter out dependencies that correspond to local workspace crates.

    Args:
        deps: List of dependency strings
        local_crate_names: Set of crate names that are workspace members

    Returns:
        Filtered list of dependencies with local crates excluded
    """
    # Convert local crate names to their Debian package name prefixes
    # We check prefixes because feature variants have the form:
    # librust-foo-dev (base) or librust-foo+feature-dev (with features)
    local_package_prefixes = {
        debcargo_binary_name(name)[:-4]  # Remove "-dev" suffix to get prefix
        for name in local_crate_names
    }

    filtered_deps = []
    for dep in deps:
        # Parse the dependency to get the package name
        parsed = PkgRelation.parse_relations(dep)
        # Check if any of the alternatives is a local package or feature variant
        is_local = False
        for alternatives in parsed:
            for rel in alternatives:
                pkg_name = rel["name"]
                # Check if package name starts with any local package prefix
                # and ends with "-dev" (to avoid false positives)
                if pkg_name.endswith("-dev"):
                    pkg_prefix = pkg_name[:-4]  # Remove "-dev"
                    # Check if it matches a local crate (base, with features, or with version)
                    # Format: librust-cratename, librust-cratename+feature,
                    # librust-cratename-0.1, librust-cratename-0.1+feature, etc.
                    for local_prefix in local_package_prefixes:
                        if (
                            pkg_prefix == local_prefix
                            or pkg_prefix.startswith(local_prefix + "+")
                            or pkg_prefix.startswith(local_prefix + "-")
                        ):
                            is_local = True
                            logging.info(f"  Excluding local crate: {pkg_name}")
                            break
                if is_local:
                    break
            if is_local:
                break

        if not is_local:
            filtered_deps.append(dep)

    return filtered_deps


def update_build_dependencies(
    control_path: str, new_deps: List[str], drop_unreferenced: bool = False
) -> None:
    """Update Build-Depends field in debian/control.

    Args:
        control_path: Path to the debian/control file
        new_deps: List of new dependency strings to add/update
        drop_unreferenced: If True, remove librust-*-dev dependencies not in new_deps
    """
    with ControlEditor(control_path) as editor:
        source = editor.source

        # Get package names from new_deps for comparison
        new_pkg_names = set()
        for dep in new_deps:
            parsed = PkgRelation.parse_relations(dep)
            for alternatives in parsed:
                for rel in alternatives:
                    new_pkg_names.add(rel["name"])

        # If dropping unreferenced, remove librust-*-dev packages not in new_deps
        dropped_count = 0
        if drop_unreferenced:
            # Get existing Build-Depends
            existing_deps = source.get("Build-Depends", "")
            parsed_existing = (
                PkgRelation.parse_relations(existing_deps) if existing_deps else []
            )

            # Find librust-*-dev packages to drop
            packages_to_drop = []
            for alternatives in parsed_existing:
                for rel in alternatives:
                    pkg_name = rel["name"]
                    # Check if it's a librust-*-dev package not in new dependencies
                    if pkg_name.startswith("librust-") and pkg_name.endswith("-dev"):
                        if pkg_name not in new_pkg_names:
                            packages_to_drop.append(pkg_name)

            # Drop unreferenced packages using PkgRelationFieldEditor
            if packages_to_drop:
                with PkgRelationFieldEditor(source, "Build-Depends") as build_deps:
                    for pkg_name in packages_to_drop:
                        build_deps.drop_relation(pkg_name)
                        dropped_count += 1
                        logging.info(f"  Dropping: {pkg_name}")

        # Get existing Build-Depends (after potential drops)
        existing_deps = source.get("Build-Depends", "")
        updated_deps = existing_deps

        # Use ensure_relation for each dependency
        added_count = 0
        for dep in new_deps:
            # Store the original to detect changes
            before = updated_deps
            updated_deps = ensure_relation(updated_deps, dep)

            # Check if the relation was added
            if updated_deps != before:
                added_count += 1
                logging.info(f"  Adding: {dep}")

        # Update the Build-Depends field
        source["Build-Depends"] = updated_deps

        if dropped_count > 0:
            logging.info(
                f"\nDropped {dropped_count} unreferenced librust-*-dev dependencies"
            )
        logging.info(f"Added {added_count} new build dependencies")


def main() -> int:
    """Main entry point."""
    parser = argparse.ArgumentParser(
        description="Update debian/control build dependencies from debcargo output."
    )
    parser.add_argument(
        "--drop-unreferenced",
        action="store_true",
        help="Drop librust-*-dev dependencies that are not referenced by any workspace crate",
    )
    parser.add_argument(
        "--exclude-local-crates",
        action="store_true",
        default=True,
        help="Exclude dependencies for crates that are present as workspace members (default)",
    )
    parser.add_argument(
        "--include-local-crates",
        action="store_false",
        dest="exclude_local_crates",
        help="Include dependencies for crates that are present as workspace members",
    )
    parser.add_argument(
        "--features",
        action="append",
        help="Features to include in dependencies (can be specified multiple times)",
    )
    parser.add_argument(
        "--all-features",
        action="store_true",
        help="Include all features in dependencies",
    )
    parser.add_argument(
        "--no-default-features",
        action="store_true",
        help="Do not include default feature in dependencies",
    )
    parser.add_argument(
        "--allow-prerelease-deps",
        action="store_true",
        help="Allow prerelease versions of dependencies",
    )
    parser.add_argument(
        "--include-dev-dependencies",
        action="store_true",
        help="Include dev dependencies",
    )
    args = parser.parse_args()

    logging.basicConfig(level=logging.INFO, format="%(message)s")

    control_path = "debian/control"

    # Discover all workspace crates
    logging.info("Discovering workspace crates...")
    crate_manifests, crate_names = discover_workspace_crates()
    logging.info(f"Found {len(crate_manifests)} workspace crates:")
    for manifest in crate_manifests:
        logging.info(f"  - {manifest}")

    # Collect all dependencies from all crates
    all_deps: Set[str] = set()
    for manifest in crate_manifests:
        logging.info(
            f"\nRunning debcargo deb-dependencies on {manifest.relative_to(Path.cwd())}..."
        )
        deps = get_debcargo_dependencies(
            manifest,
            features=args.features,
            all_features=args.all_features,
            no_default_features=args.no_default_features,
            allow_prerelease_deps=args.allow_prerelease_deps,
            include_dev_dependencies=args.include_dev_dependencies,
        )
        logging.info(f"  Found {len(deps)} dependencies")
        all_deps.update(deps)

    logging.info(f"\nTotal unique dependencies across all crates: {len(all_deps)}")

    # Filter out local crate dependencies if requested
    deps_to_add = sorted(all_deps)
    if args.exclude_local_crates:
        logging.info("\nFiltering out local workspace crates...")
        deps_to_add = filter_local_crate_dependencies(deps_to_add, crate_names)
        logging.info(f"Remaining dependencies after filtering: {len(deps_to_add)}")

    # Update debian/control with all dependencies
    logging.info(f"\nUpdating {control_path}...")
    update_build_dependencies(
        control_path, deps_to_add, drop_unreferenced=args.drop_unreferenced
    )

    logging.info("\nDone!")
    return 0


if __name__ == "__main__":
    sys.exit(main())