Coverage for rust2rpm/project/local.py: 96%
80 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-26 14:01 +0100
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-26 14:01 +0100
1"""Module containing functionality for loading projects from unpacked local sources."""
3import contextlib
4import shutil
5import tempfile
6from collections.abc import Generator
7from dataclasses import dataclass
8from pathlib import Path
9from typing import Any
11from cargo2rpm.metadata import Metadata, Package
12from cargo2rpm.semver import Version
14from rust2rpm import log
15from rust2rpm.cli import Options
16from rust2rpm.conf import TomlConf
17from rust2rpm.inspect import get_doc_files, get_license_files
18from rust2rpm.metadata import guess_main_package
19from rust2rpm.patching import make_patches
20from rust2rpm.vendor import generate_vendor_tarball
22from .meta import PatchFile, Project
23from .shared import file_name_auto_patch, file_name_manual_patch
26@contextlib.contextmanager
27def temp_project_copy(path: Path) -> Generator[Path, Any, None]:
28 """Provide a copy of the project sources in a temporary directory."""
29 with tempfile.TemporaryDirectory() as tmpdir:
30 target = Path(tmpdir) / path.name
31 yield shutil.copytree(path, target)
34def guess_local_project_version_from_dir(dir_name: str) -> tuple[str | None, str | None]:
35 """Guess name and version of a local project based on the name of the parent directory.
37 The heuristic used in this function does not work for pre-release versions,
38 since a default format of `{name}-{version}` is assumed, but pre-releases
39 contain a `-` character too.
41 Arguments:
42 dir_name: Name of the directory.
44 Returns:
45 Tuple of (name, version) strings, or (`None`, `None`) in case the
46 directory name could not be parsed.
48 """
49 project = dir_name.rstrip("0123456789.").removesuffix("-")
50 version = dir_name.removeprefix(f"{project}-")
52 try:
53 Version.parse(version)
54 except ValueError:
55 return None, None
56 else:
57 return project, version
60def guess_local_project_version_from_path(path: Path, version: str | None) -> tuple[str | None, str | None]:
61 """Guess name and version of a local project based on the project path and version.
63 If the version is specified, it is stripped from the name of the parent
64 directory, and the remaining string is assumed to the the project name.
66 If the version is not specified, the name of the parent directory is
67 attempted to be parsed based on the assumption that it is named according to
68 the common `{name}-{version}` format.
70 Arguments:
71 path: Path to the project's root directory or to the Cargo.toml file at
72 the project root.
73 version: Optional version string. This is useful for pre-releases (which
74 otherwise cannot be parsed correctly) or if the upstream project
75 releases tarballs with non-SemVer version strings.
77 Returns:
78 Tuple of (name, version) strings, or (`None`, `None`) in case the
79 directory name could not be parsed.
81 """
82 dir_name = path.resolve().name if path.is_dir() else path.resolve().parent.name
84 if version: 84 ↛ 88line 84 didn't jump to line 88 because the condition on line 84 was always true
85 project = dir_name.removesuffix(f"-{version}")
86 return project, version
88 return guess_local_project_version_from_dir(dir_name)
91@dataclass(frozen=True)
92class LocalProject(Project):
93 """Description of a project based on locally unpacked sources."""
95 main_package: Package
96 """Heuristically determined "main" package of the cargo workspace.
98 In the case of local sources that contain a single crate (i.e. which are not
99 cargo workspaces), this contains the metadata of this single crate.
100 """
102 @property
103 def is_workspace(self) -> bool:
104 """Predicate that is True for cargo workspaces and False for single crates."""
105 return len(self.metadata.packages) > 1
107 @property
108 def rpm_name(self) -> str:
109 """RPM source package name based on project metadata."""
110 return self.name
112 @staticmethod
113 def from_cargo_toml_path(
114 path: Path,
115 name: str | None,
116 version: str | None,
117 out_dir: Path | None,
118 conf: TomlConf,
119 options: Options,
120 ) -> "LocalProject":
121 """Load project metadata based on the contents of an unpacked source tree.
123 Arguments:
124 path: Path to the Cargo.toml file at the root of the project.
125 name: Optional name hint for the project in case it cannot be
126 guessed from the name of the parent directory.
127 version: Optional version hint for the project in case it cannot be
128 guessed from the name of the parent directory.
129 out_dir: Output directory for any generated or downloaded files.
130 conf: Global rust2rpm configuration.
131 options: Options and flags derived from command-line arguments.
133 Returns:
134 Project metadata based on the contents of the unpacked source tree.
136 """
137 project_root = path.parent
138 doc_files = get_doc_files(project_root, conf)
139 license_files = get_license_files(project_root, conf)
141 metadata = Metadata.from_cargo(str(path))
142 parent_dir = Path(project_root).parent
144 is_workspace = len(metadata.packages) > 1
146 if is_workspace:
147 log.info("Skipping automatic creation of patches for cargo workspace.")
149 # fall back to the directory name for determining the name / version
150 # of the project heuristically
151 guessed_name, guessed_version = guess_local_project_version_from_path(path, version)
153 main_package = guess_main_package(metadata, name, guessed_name)
155 # prefer explicit arguments over guesses over conjectures
156 name = name or guessed_name or main_package.name
157 version = version or guessed_version or main_package.version
159 log.warn(
160 f"Falling back to {name!r} as the main package. If this is not correct, "
161 "specify the correct name in the 'pkgid' argument on the command line.",
162 )
164 vendor_tarball = generate_vendor_tarball(path, name, version, out_dir or parent_dir, options.vendor)
166 return LocalProject(
167 name,
168 version,
169 metadata,
170 license_files,
171 doc_files,
172 None,
173 None,
174 vendor_tarball,
175 main_package,
176 )
178 # RET505: removing this else branch makes this less readable
179 else: # noqa: RET505
180 package = metadata.packages[0]
182 name = package.name
183 version = package.version
185 reapply_path = (out_dir or Path()) / file_name_manual_patch(name) if options.reuse_patch else None
187 with temp_project_copy(project_root) as temp_root:
188 temp_toml = temp_root / "Cargo.toml"
190 auto_diff, manual_diff = make_patches(
191 name,
192 package.version,
193 temp_toml,
194 reapply_path,
195 options,
196 )
198 # ensure metadata is up-to-date with changes from patches
199 metadata = Metadata.from_cargo(str(temp_toml))
201 vendor_tarball = generate_vendor_tarball(
202 temp_toml,
203 name,
204 version,
205 out_dir or parent_dir,
206 options.vendor,
207 )
209 auto_patch = PatchFile(file_name_auto_patch(name), diff) if (diff := auto_diff) else None
210 manual_patch = PatchFile(file_name_manual_patch(name), diff) if (diff := manual_diff) else None
212 return LocalProject(
213 name,
214 version,
215 metadata,
216 license_files,
217 doc_files,
218 auto_patch,
219 manual_patch,
220 vendor_tarball,
221 package,
222 )
224 @staticmethod
225 def from_project_folder(
226 path: Path,
227 name: str | None,
228 version: str | None,
229 out_dir: Path | None,
230 conf: TomlConf,
231 options: Options,
232 ) -> "LocalProject":
233 """Load project metadata based on the contents of an unpacked source tree.
235 This method is a thin wrapper around `LocalProject.from_cargo_toml_path`.
237 Arguments:
238 path: Path to the root directory of the unpacked sources.
239 name: Optional name hint for the project in case it cannot be
240 guessed from the name of the parent directory.
241 version: Optional version hint for the project in case it cannot be
242 guessed from the name of the parent directory.
243 out_dir: Output directory for any generated or downloaded files.
244 conf: Global rust2rpm configuration.
245 options: Options and flags derived from command-line arguments.
247 Returns:
248 Project metadata based on the contents of the unpacked source tree.
250 """
251 toml_path = path / "Cargo.toml"
253 return LocalProject.from_cargo_toml_path(
254 toml_path,
255 name,
256 version,
257 out_dir,
258 conf,
259 options,
260 )