crate: improve determining project name/version from directory name

If the heuristics fail because the project uses a weird naming or
versioning scheme or if the directory name does not match the
"{project}-{version}" pattern. This is mostly the case for "workspace"
proejcts.

In the case the heuristics fail, the version can be overriden on the
command line.
This commit is contained in:
Fabio Valentini 2023-11-29 18:34:08 +01:00
parent 323dbea4b6
commit 228efa58fd
No known key found for this signature in database
GPG key ID: 5AC5F572E5D410AF
4 changed files with 91 additions and 8 deletions

View file

@ -8,7 +8,7 @@ from cargo2rpm.semver import Version
from rust2rpm import log from rust2rpm import log
from rust2rpm.cli import get_parser from rust2rpm.cli import get_parser
from rust2rpm.conf import load_config from rust2rpm.conf import load_config
from rust2rpm.crate import InvalidProjectError, process_project from rust2rpm.crate import InvalidProjectError, InvalidVersionError, process_project
from rust2rpm.cratesio import NoVersionsError from rust2rpm.cratesio import NoVersionsError
from rust2rpm.distgit import get_package_info from rust2rpm.distgit import get_package_info
from rust2rpm.generator import spec_render_crate, spec_render_project, spec_render_workspace from rust2rpm.generator import spec_render_crate, spec_render_project, spec_render_workspace
@ -56,6 +56,12 @@ def main():
except InvalidProjectError: except InvalidProjectError:
log.error(f"Invalid argument: {args.crate!r} is not a local file or directory") log.error(f"Invalid argument: {args.crate!r} is not a local file or directory")
sys.exit(1) sys.exit(1)
except InvalidVersionError:
log.error(
f"Could not determine project name and version automatically. "
"Please specify the project version on the command line."
)
sys.exit(1)
if metadata.is_workspace(): if metadata.is_workspace():
base_name = project base_name = project

View file

@ -58,6 +58,10 @@ class InvalidProjectError(ValueError):
pass pass
class InvalidVersionError(ValueError):
pass
def local_toml_file(toml_path: str) -> tuple[str, list[str], list[str]]: def local_toml_file(toml_path: str) -> tuple[str, list[str], list[str]]:
assert os.path.isfile(toml_path) assert os.path.isfile(toml_path)
assert os.path.basename(toml_path) == "Cargo.toml" assert os.path.basename(toml_path) == "Cargo.toml"
@ -149,14 +153,52 @@ def project_is_path(path: str) -> bool:
return "/" in path or path in {".", ".."} return "/" in path or path in {".", ".."}
def guess_local_project_version_from_path(project: str) -> tuple[str, str]: def guess_local_project_version_from_dir(dir_name: str) -> tuple[str, str]:
"""
Use a simple heuristic to determine the project name and version from the
name of the directory that contains the Cargo.toml file.
Raises an InvalidVersionError if the automatically determined version is
not valid according to SemVer.
"""
project = dir_name.rstrip("0123456789.").removesuffix("-")
version = dir_name.removeprefix(f"{project}-")
try:
Version.parse(version)
except ValueError as exc:
raise InvalidVersionError(exc.args)
return project, version
def guess_local_project_version_from_path(project: str, version: Optional[str]) -> tuple[str, str]:
"""
Use a simple heuristic to determine the project name and version from the
"project" argument supplied on the command line.
If the argument points at a file (i.e. a Cargo.toml file), the heuristics
use the name of the file's parent directory. If the argument points at a
directory, the name of the directory itself is used.
Raises an InvalidVersionError if the heuristics for automatically
determining the project name and version fail, or if the automatically
determined version is not valid according to SemVer. In this case,
supplying the optional "version" argument on the command line can override
the version string.
"""
if os.path.isdir(project): if os.path.isdir(project):
dir_name = os.path.split(os.path.abspath(project))[1] dir_name = os.path.split(os.path.abspath(project))[1]
else: else:
dir_name = os.path.split(os.path.dirname(os.path.abspath(project)))[1] dir_name = os.path.split(os.path.dirname(os.path.abspath(project)))[1]
project = dir_name.rstrip("0123456789.").removesuffix("-")
version = dir_name.removeprefix(f"{project}-") if version:
return project, version project = dir_name.removesuffix(f"-{version}")
return project, version
else:
return guess_local_project_version_from_dir(dir_name)
@contextlib.contextmanager @contextlib.contextmanager
@ -170,6 +212,7 @@ def toml_temp_copy(toml_path: str):
def process_project_local( def process_project_local(
project: str, project: str,
version: Optional[str],
patch: bool, patch: bool,
patch_foreign: bool, patch_foreign: bool,
vendor: bool, vendor: bool,
@ -188,7 +231,7 @@ def process_project_local(
# fall back to the directory name for determining the name / version # fall back to the directory name for determining the name / version
# of the project heuristically # of the project heuristically
name, version = guess_local_project_version_from_path(project) name, version = guess_local_project_version_from_path(project, version)
log.warn(f"Falling back to {name!r} as the name of the project (based on the name of the containing folder).") log.warn(f"Falling back to {name!r} as the name of the project (based on the name of the containing folder).")
diffs: tuple[Optional[list[str]], Optional[list[str]]] = (None, None) diffs: tuple[Optional[list[str]], Optional[list[str]]] = (None, None)
@ -276,7 +319,7 @@ def process_project(
log.warn("The '--store-crate' flag has no effect for unpacked sources.") log.warn("The '--store-crate' flag has no effect for unpacked sources.")
name, version, diffs, metadata, doc_files, license_files, vendor_tarball = process_project_local( name, version, diffs, metadata, doc_files, license_files, vendor_tarball = process_project_local(
project, patch, patch_foreign, vendor project, version, patch, patch_foreign, vendor
) )
return name, version, diffs, metadata, doc_files, license_files, True, vendor_tarball return name, version, diffs, metadata, doc_files, license_files, True, vendor_tarball

View file

@ -533,7 +533,12 @@ def spec_render_workspace(
is_cdylib = metadata.is_cdylib() is_cdylib = metadata.is_cdylib()
rpm_version = Version.parse(upstream_version).to_rpm() try:
rpm_version = Version.parse(upstream_version).to_rpm()
except ValueError:
log.warn(f"Version {upstream_version!r} is not valid according to SemVer.")
rpm_version = upstream_version
rpm_description = main_package.get_description() rpm_description = main_package.get_description()
rpm_summary = main_package.get_summary() rpm_summary = main_package.get_summary()

View file

@ -0,0 +1,29 @@
import pytest
from rust2rpm.crate import guess_local_project_version_from_dir
@pytest.mark.parametrize(
"path,project,version",
[
("project-1.2.3", "project", "1.2.3"),
("test-project-1.2.3", "test-project", "1.2.3"),
],
ids=repr,
)
def test_guess_local_project_version_from_dir(path: str, project: str, version: str):
p, v = guess_local_project_version_from_dir(path)
assert (p, v) == (project, version)
@pytest.mark.parametrize(
"path,error",
[
("helix-23.10-source", "Invalid version"),
],
ids=repr,
)
def test_guess_local_project_version_from_dir_fail(path: str, error: str):
with pytest.raises(ValueError) as exc:
guess_local_project_version_from_dir(path)
assert error in str(exc.value)