diff --git a/.gitignore b/.gitignore index 2d7ad2f..b6f8276 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ /build/ /venv/ /.idea/ +/.coverage diff --git a/rust2rpm/__main__.py b/rust2rpm/__main__.py index ffd95ca..2c333f9 100644 --- a/rust2rpm/__main__.py +++ b/rust2rpm/__main__.py @@ -1,4 +1,3 @@ -import configparser import os import pathlib import sys @@ -7,6 +6,7 @@ from cargo2rpm.metadata import FeatureFlags from rust2rpm import log from rust2rpm.cli import get_parser +from rust2rpm.conf import Rust2RpmConf, Rust2RpmConfError from rust2rpm.crate import process_project from rust2rpm.cratesio import NoVersionsError from rust2rpm.distgit import get_package_info @@ -84,30 +84,28 @@ def main(): packager = detect_packager() - conf = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation()) - confs = conf.read([".rust2rpm.conf", "_rust2rpm.conf", "rust2rpm.conf"]) + try: + # known file names (only in the current working directory) + filenames = [".rust2rpm.conf", "_rust2rpm.conf", "rust2rpm.conf"] - # clean up configuration files with deprecated names - if len(confs) > 1: + if not metadata.is_workspace(): + distconf = Rust2RpmConf.load(filenames, args.target, metadata.packages[0].get_feature_names()) + else: + distconf = Rust2RpmConf.load(filenames, args.target, set()) + + except FileExistsError: log.error( "More than one *rust2rpm.conf file is present in this directory. " + "Ensure that there is only one, and that it has the correct contents." ) sys.exit(1) - if ".rust2rpm.conf" in confs and "rust2rpm.conf" not in confs: - os.rename(".rust2rpm.conf", "rust2rpm.conf") - log.info("Renamed deprecated, hidden .rust2rpm.conf file to rust2rpm.conf.") + except Rust2RpmConfError as exc: + log.error("Invalid rust2rpm configuration file:") + log.error(str(exc)) + sys.exit(1) - if "_rust2rpm.conf" in confs and "rust2rpm.conf" not in confs: - os.rename("_rust2rpm.conf", "rust2rpm.conf") - log.info("Renamed deprecated _rust2rpm.conf file to rust2rpm.conf.") - - if args.target not in conf: - conf.add_section(args.target) - - conf_all_features = conf[args.target].getboolean("all-features") - if conf_all_features is False and args.all_features: + if distconf.all_features is False and args.all_features: log.warn( 'Conflicting settings for enabling all features: The setting is "false"' + 'in rust2rpm.conf but it was enabled with the "--all-features" CLI flag.' @@ -122,13 +120,14 @@ def main(): patch_file_manual=patch_files[1], license_files=license_files, doc_files=doc_files, - distconf=conf[args.target], - feature_flags=FeatureFlags(all_features=(conf_all_features or args.all_features)), + distconf=distconf, + feature_flags=FeatureFlags(all_features=(distconf.all_features or args.all_features)), relative_license_paths=args.relative_license_paths, rpmautospec=args.rpmautospec, auto_changelog_entry=args.auto_changelog_entry, packager=packager, ) + else: spec_contents = spec_render_workspace( metadata=metadata, @@ -136,8 +135,8 @@ def main(): rpm_name=rpm_name, license_files=license_files, doc_files=doc_files, - distconf=conf[args.target], - feature_flags=FeatureFlags(all_features=(conf_all_features or args.all_features)), + distconf=distconf, + feature_flags=FeatureFlags(all_features=(distconf.all_features or args.all_features)), rpmautospec=args.rpmautospec, auto_changelog_entry=args.auto_changelog_entry, packager=packager, diff --git a/rust2rpm/conf.py b/rust2rpm/conf.py new file mode 100644 index 0000000..404e978 --- /dev/null +++ b/rust2rpm/conf.py @@ -0,0 +1,112 @@ +import configparser +import os +from typing import Optional + +from rust2rpm import log + + +def to_list(s): + if not s: + return [] + return list(sorted(filter(None, (l.strip() for l in s.splitlines())))) + + +class Rust2RpmConfError(ValueError): + pass + + +class Rust2RpmConf: + def __init__( + self, + *, + all_features: bool = False, + unwanted_features: list[str] = None, + buildrequires: list[str] = None, + testrequires: list[str] = None, + bin_requires: list[str] = None, + lib_requires: dict[Optional[str], list[str]] = None, + ): + self.all_features: bool = all_features + self.unwanted_features: list[str] = unwanted_features or list() + self.buildrequires: list[str] = buildrequires or list() + self.testrequires: list[str] = testrequires or list() + self.bin_requires: list[str] = bin_requires or list() + self.lib_requires: dict[Optional[str], list[str]] = lib_requires or dict() + + @staticmethod + def load(filenames: list[str], target: str, features: set[str]): + conf = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation()) + confs = conf.read(filenames) + + if len(confs) > 1: + raise FileExistsError + + # clean up configuration files with deprecated names + if ".rust2rpm.conf" in confs: + os.rename(".rust2rpm.conf", "rust2rpm.conf") + log.info("Renamed deprecated, hidden .rust2rpm.conf file to rust2rpm.conf.") + + if "_rust2rpm.conf" in confs: + os.rename("_rust2rpm.conf", "rust2rpm.conf") + log.info("Renamed deprecated _rust2rpm.conf file to rust2rpm.conf.") + + # merge target-specific configuration with default configuration + if target not in conf: + conf.add_section(target) + merged = conf[target] + + # validate configuration file + valid_targets = ["fedora", "mageia", "opensuse", "plain"] + valid_keys = [ + "all-features", + "unwanted-features", + "buildrequires", + "testrequires", + "lib.requires", + "bin.requires", + ] + for feature in features: + valid_keys.append(f"lib+{feature}.requires") + + # check section names + for section in conf.keys(): + if section != "DEFAULT" and section not in valid_targets: + raise Rust2RpmConfError(f"Invalid section: {section!r}") + + # check setting keys + for key in merged.keys(): + if key not in valid_keys: + raise Rust2RpmConfError(f"Invalid key: {key!r}") + + # parse configuration and validate settings + settings = dict() + + if all_features := merged.getboolean("all-features") and all_features is not None: + settings["all_features"] = all_features + + if unwanted_features := merged.get("unwanted-features"): + settings["unwanted_features"] = to_list(unwanted_features) + + for unwanted_feature in settings["unwanted_features"]: + if unwanted_feature not in features: + raise Rust2RpmConfError(f'Unrecognized "unwanted" feature: {unwanted_feature!r}') + + if buildrequires := merged.get("buildrequires"): + settings["buildrequires"] = to_list(buildrequires) + if testrequires := merged.get("testrequires"): + settings["testrequires"] = to_list(testrequires) + if bin_requires := merged.get("bin.requires"): + settings["bin_requires"] = to_list(bin_requires) + + if lib_requires := merged.get("lib.requires"): + if "lib_requires" not in settings.keys(): + settings["lib_requires"] = dict() + settings["lib_requires"][None] = to_list(lib_requires) + + for feature in features: + if lib_feature_requires := merged.get(f"lib+{feature}.requires"): + if "lib_requires" not in settings.keys(): + settings["lib_requires"] = dict() + settings["lib_requires"][feature] = to_list(lib_feature_requires) + + return Rust2RpmConf(**settings) diff --git a/rust2rpm/generator.py b/rust2rpm/generator.py index a70b590..087dded 100644 --- a/rust2rpm/generator.py +++ b/rust2rpm/generator.py @@ -10,6 +10,7 @@ from cargo2rpm import rpm import jinja2 from rust2rpm import __version__, log +from rust2rpm.conf import Rust2RpmConf from rust2rpm.licensing import translate_license from rust2rpm.metadata import guess_main_package, package_uses_rust_1_60_feature_syntax @@ -108,7 +109,7 @@ def spec_render_crate( patch_file_manual: Optional[str], license_files: list[str], doc_files: list[str], - distconf, + distconf: Rust2RpmConf, feature_flags: FeatureFlags, relative_license_paths: bool, rpmautospec: bool, @@ -149,25 +150,15 @@ def spec_render_crate( rpm_requires = {feature: list(sorted(rpm.requires(package, feature))) for feature in features} rpm_provides = {feature: rpm.provides(package, feature) for feature in features} - conf_buildrequires = to_list(distconf.get("buildrequires")) - conf_buildrequires.sort() - - conf_test_requires = to_list(distconf.get("testrequires")) - conf_test_requires.sort() - - conf_bin_requires = to_list(distconf.get("bin.requires")) - conf_bin_requires.sort() - conf_lib_requires = dict() for feature in features: if feature is None: conf_key = "lib" else: conf_key = f"lib+{feature}" - conf_lib_requires[conf_key] = to_list(distconf.get(f"{conf_key}.requires")) + conf_lib_requires[conf_key] = distconf.lib_requires.get(feature) or list() - conf_unwanted_features = to_list(distconf.get("unwanted-features")) - for feature in conf_unwanted_features: + for feature in distconf.unwanted_features: features.remove(feature) if package_uses_rust_1_60_feature_syntax(package.features): @@ -219,9 +210,9 @@ def spec_render_crate( "crate_version": package.version, "crate_license": package.license, # Parameters derived from rust2rpm.conf - "conf_buildrequires": conf_buildrequires, - "conf_test_requires": conf_test_requires, - "conf_bin_requires": conf_bin_requires, + "conf_buildrequires": distconf.buildrequires, + "conf_test_requires": distconf.testrequires, + "conf_bin_requires": distconf.bin_requires, "conf_lib_requires": conf_lib_requires, # Parameters derived from command-line flags "cargo_args": cargo_args, @@ -260,7 +251,7 @@ def spec_render_workspace( rpm_name: str, license_files: list[str], doc_files: list[str], - distconf, + distconf: Rust2RpmConf, feature_flags: FeatureFlags, rpmautospec: bool, auto_changelog_entry: bool, @@ -291,15 +282,6 @@ def spec_render_workspace( rpm_buildrequires = list(sorted(buildrequires)) rpm_test_requires = list(sorted(test_requires)) - conf_buildrequires = to_list(distconf.get("buildrequires")) - conf_buildrequires.sort() - - conf_test_requires = to_list(distconf.get("testrequires")) - conf_test_requires.sort() - - conf_bin_requires = to_list(distconf.get("bin.requires")) - conf_bin_requires.sort() - if any(package_uses_rust_1_60_feature_syntax(package.features) for package in metadata.packages): rust_packaging_dep = "cargo-rpm-macros >= 24" else: @@ -346,9 +328,9 @@ def spec_render_workspace( "rpm_binary_names": binaries, "rpm_cdylib_package": is_cdylib, # Parameters derived from rust2rpm.conf - "conf_buildrequires": conf_buildrequires, - "conf_test_requires": conf_test_requires, - "conf_bin_requires": conf_bin_requires, + "conf_buildrequires": distconf.buildrequires, + "conf_test_requires": distconf.testrequires, + "conf_bin_requires": distconf.bin_requires, # Parameters derived from command-line flags "cargo_args": cargo_args, "use_rpmautospec": rpmautospec, diff --git a/rust2rpm/tests/samples/glib-sys-0.17.2.rust2rpm.conf b/rust2rpm/tests/samples/glib-sys-0.17.2.rust2rpm.conf new file mode 100644 index 0000000..b281f62 --- /dev/null +++ b/rust2rpm/tests/samples/glib-sys-0.17.2.rust2rpm.conf @@ -0,0 +1,25 @@ +[DEFAULT] +unwanted-features = + v2_76 +buildrequires = + pkgconfig(glib-2.0) >= 2.56 +lib.requires = + pkgconfig(glib-2.0) >= 2.56 +lib+v2_58.requires = + pkgconfig(glib-2.0) >= 2.58 +lib+v2_60.requires = + pkgconfig(glib-2.0) >= 2.60 +lib+v2_62.requires = + pkgconfig(glib-2.0) >= 2.62 +lib+v2_64.requires = + pkgconfig(glib-2.0) >= 2.64 +lib+v2_66.requires = + pkgconfig(glib-2.0) >= 2.66 +lib+v2_68.requires = + pkgconfig(glib-2.0) >= 2.68 +lib+v2_70.requires = + pkgconfig(glib-2.0) >= 2.70 +lib+v2_72.requires = + pkgconfig(glib-2.0) >= 2.72 +lib+v2_74.requires = + pkgconfig(glib-2.0) >= 2.74 diff --git a/rust2rpm/tests/samples/libsqlite3-sys-0.25.2.rust2rpm.conf b/rust2rpm/tests/samples/libsqlite3-sys-0.25.2.rust2rpm.conf new file mode 100644 index 0000000..4dd287e --- /dev/null +++ b/rust2rpm/tests/samples/libsqlite3-sys-0.25.2.rust2rpm.conf @@ -0,0 +1,8 @@ +[DEFAULT] +buildrequires = + pkgconfig(sqlcipher) + pkgconfig(sqlite3) >= 3.7.16 +lib.requires = + pkgconfig(sqlite3) >= 3.7.16 +lib+sqlcipher.requires = + pkgconfig(sqlcipher) diff --git a/rust2rpm/tests/test_conf.py b/rust2rpm/tests/test_conf.py new file mode 100644 index 0000000..702bdbd --- /dev/null +++ b/rust2rpm/tests/test_conf.py @@ -0,0 +1,63 @@ +from importlib import resources +from typing import Optional + +import pytest + +from rust2rpm.conf import Rust2RpmConf + + +@pytest.mark.parametrize( + "filename,features,all_features,unwanted_features,buildrequires,testrequires,bin_requires,lib_requires", + [ + ( + "libsqlite3-sys-0.25.2.rust2rpm.conf", + {"sqlcipher"}, + False, + list(), + ["pkgconfig(sqlcipher)", "pkgconfig(sqlite3) >= 3.7.16"], + list(), + list(), + {None: ["pkgconfig(sqlite3) >= 3.7.16"], "sqlcipher": ["pkgconfig(sqlcipher)"]}, + ), + ( + "glib-sys-0.17.2.rust2rpm.conf", + {"v2_58", "v2_60", "v2_62", "v2_64", "v2_66", "v2_68", "v2_70", "v2_72", "v2_74", "v2_76"}, + False, + ["v2_76"], + ["pkgconfig(glib-2.0) >= 2.56"], + list(), + list(), + { + None: ["pkgconfig(glib-2.0) >= 2.56"], + "v2_58": ["pkgconfig(glib-2.0) >= 2.58"], + "v2_60": ["pkgconfig(glib-2.0) >= 2.60"], + "v2_62": ["pkgconfig(glib-2.0) >= 2.62"], + "v2_64": ["pkgconfig(glib-2.0) >= 2.64"], + "v2_66": ["pkgconfig(glib-2.0) >= 2.66"], + "v2_68": ["pkgconfig(glib-2.0) >= 2.68"], + "v2_70": ["pkgconfig(glib-2.0) >= 2.70"], + "v2_72": ["pkgconfig(glib-2.0) >= 2.72"], + "v2_74": ["pkgconfig(glib-2.0) >= 2.74"], + }, + ), + ], +) +def test_rust2rpm_conf_load( + filename: str, + features: set[str], + all_features: bool, + unwanted_features: list[str], + buildrequires: list[str], + testrequires: list[str], + bin_requires: list[str], + lib_requires: dict[Optional[str], list[str]], +): + path = str(resources.files("rust2rpm.tests.samples").joinpath(filename)) + conf = Rust2RpmConf.load(path, "fedora", features) + + assert conf.all_features == all_features + assert conf.unwanted_features == unwanted_features + assert conf.buildrequires == buildrequires + assert conf.testrequires == testrequires + assert conf.bin_requires == bin_requires + assert conf.lib_requires == lib_requires diff --git a/rust2rpm/tests/test_generator.py b/rust2rpm/tests/test_generator.py index 593bc73..73aa399 100644 --- a/rust2rpm/tests/test_generator.py +++ b/rust2rpm/tests/test_generator.py @@ -8,6 +8,7 @@ from cargo2rpm.metadata import Metadata, FeatureFlags import pytest from rust2rpm.cli import get_parser +from rust2rpm.conf import Rust2RpmConf from rust2rpm.generator import to_list, spec_render_crate, spec_render_workspace from rust2rpm.patching import drop_foreign_dependencies from rust2rpm.utils import package_name_suffixed @@ -51,7 +52,7 @@ def test_spec_file_render_crate(filename: str, target: str, tmp_path: Path): patch_file_manual=f"{crate}-patch2.diff", license_files=["LIC1", "LIC2"], doc_files=["DOC1", "DOC2"], - distconf={}, + distconf=Rust2RpmConf(), feature_flags=FeatureFlags(), relative_license_paths=False, rpmautospec=target == "fedora", @@ -64,7 +65,7 @@ def test_spec_file_render_crate(filename: str, target: str, tmp_path: Path): fixture_path = resources.files("rust2rpm.tests.samples").joinpath(f"{crate_name_version}.{target}.spec") - if os.getenv("UPDATE_FIXTURES") == "1": + if os.getenv("UPDATE_FIXTURES") == "1": # pragma nocover # helper mode to create test data fixture_path.write_text(rendered) @@ -88,7 +89,7 @@ def test_spec_file_render_workspace(filename: str, target: str, tmp_path: Path): rpm_name=crate, license_files=["LIC1", "LIC2"], doc_files=["DOC1", "DOC2"], - distconf={}, + distconf=Rust2RpmConf(), feature_flags=FeatureFlags(), rpmautospec=target == "fedora", auto_changelog_entry=True, @@ -100,7 +101,7 @@ def test_spec_file_render_workspace(filename: str, target: str, tmp_path: Path): fixture_path = resources.files("rust2rpm.tests.samples").joinpath(f"{crate_name_version}.{target}.spec") - if os.getenv("UPDATE_FIXTURES") == "1": + if os.getenv("UPDATE_FIXTURES") == "1": # pragma nocover # helper mode to create test data fixture_path.write_text(rendered) @@ -189,7 +190,7 @@ def test_drop_foreign_dependencies(filename: str, features: set[str], expected: toml_before = before_path.read_text().split("\n") patched = drop_foreign_dependencies(toml_before, features) or toml_before - if os.getenv("UPDATE_FIXTURES") == "1": + if os.getenv("UPDATE_FIXTURES") == "1": # pragma nocover # helper mode to create / update test fixtures after_path.write_text("\n".join(patched))