From b06b94834e1e2c2176e53bdafc8ff058ab75cd07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0oltis?= Date: Mon, 1 Jul 2024 11:55:03 +0200 Subject: [PATCH] cachi2 ruby PoC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit proof of concept for ruby package manager based on cachito implementation Signed-off-by: Michal Šoltis --- cachi2/core/models/input.py | 22 +- cachi2/core/package_managers/rubygems.py | 272 ++++++++++++++++++++++ cachi2/core/resolver.py | 3 +- docs/design/rubygems.md | 284 +++++++++++++++++++++++ pyproject.toml | 1 + requirements-extras.txt | 24 ++ requirements.txt | 30 ++- 7 files changed, 631 insertions(+), 5 deletions(-) create mode 100644 cachi2/core/package_managers/rubygems.py create mode 100644 docs/design/rubygems.md diff --git a/cachi2/core/models/input.py b/cachi2/core/models/input.py index 709671c83..2f00cf078 100644 --- a/cachi2/core/models/input.py +++ b/cachi2/core/models/input.py @@ -59,7 +59,7 @@ def show_error(error: "ErrorDict") -> str: # Supported package managers -PackageManagerType = Literal["gomod", "npm", "pip", "rpm", "yarn"] +PackageManagerType = Literal["gomod", "npm", "pip", "rpm", "rubygems", "yarn"] Flag = Literal[ "cgo-disable", "dev-package-managers", "force-gomod-tidy", "gomod-vendor", "gomod-vendor-check" @@ -179,8 +179,21 @@ class YarnPackageInput(_PackageInputBase): type: Literal["yarn"] +class RubygemsPackageInput(_PackageInputBase): + """Accepted input for a Rubygems package.""" + + type: Literal["rubygems"] + + PackageInput = Annotated[ - Union[GomodPackageInput, NpmPackageInput, PipPackageInput, RpmPackageInput, YarnPackageInput], + Union[ + GomodPackageInput, + NpmPackageInput, + PipPackageInput, + RpmPackageInput, + RubygemsPackageInput, + YarnPackageInput, + ], # https://pydantic-docs.helpmanual.io/usage/types/#discriminated-unions-aka-tagged-unions pydantic.Field(discriminator="type"), ] @@ -246,6 +259,11 @@ def rpm_packages(self) -> list[RpmPackageInput]: """Get the rpm packages specified for this request.""" return self._packages_by_type(RpmPackageInput) + @property + def rubygems_packages(self) -> list[RubygemsPackageInput]: + """Get the Rubygems packages specified for this request.""" + return self._packages_by_type(RubygemsPackageInput) + @property def yarn_packages(self) -> list[YarnPackageInput]: """Get the yarn packages specified for this request.""" diff --git a/cachi2/core/package_managers/rubygems.py b/cachi2/core/package_managers/rubygems.py new file mode 100644 index 000000000..08c3474a9 --- /dev/null +++ b/cachi2/core/package_managers/rubygems.py @@ -0,0 +1,272 @@ +import logging +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Optional + +from gemlock_parser.gemfile_lock import Gem, GemfileLockParser # type: ignore +from packageurl import PackageURL + +from cachi2.core.errors import FetchError, UnsupportedFeature +from cachi2.core.models.input import Request, RubygemsPackageInput +from cachi2.core.models.output import Component, EnvironmentVariable, ProjectFile, RequestOutput +from cachi2.core.package_managers.general import download_binary_file, extract_git_info +from cachi2.core.rooted_path import RootedPath +from cachi2.core.scm import clone_as_tarball + +GEMFILE_LOCK = "Gemfile.lock" + +GIT_REF_FORMAT = re.compile(r"^[a-fA-F0-9]{40}$") +PLATFORMS_RUBY = re.compile(r"^PLATFORMS\n {2}ruby\n\n", re.MULTILINE) + +log = logging.getLogger(__name__) + + +def fetch_rubygems_source(request: Request) -> RequestOutput: + """Resolve and fetch RubyGems dependencies.""" + components = [] + environment_variables = [ + EnvironmentVariable(name="BUNDLE_CACHE_ALL", value="true"), + EnvironmentVariable(name="BUNDLE_CACHE_PATH", value="${output_dir}/deps/rubygems"), + EnvironmentVariable(name="BUNDLE_FORCE_RUBY_PLATFORM", value="true"), + ] + project_files: list[ProjectFile] = [] + + output_dir = request.output_dir.join_within_root("deps", "rubygems") + output_dir.path.mkdir(parents=True, exist_ok=True) + + for package in request.rubygems_packages: + info = _resolve_rubygems(request.source_dir, output_dir, package) + components.append(Component.from_package_dict(info["package"])) + for dependency in info["dependencies"]: + components.append( + Component( + name=dependency["name"], + version=dependency["version"], + purl=dependency["purl"], + ) + ) + + return RequestOutput.from_obj_list( + components, + environment_variables=environment_variables, + project_files=project_files, + ) + + +def _resolve_rubygems( + source_dir: RootedPath, + output_dir: RootedPath, + package: RubygemsPackageInput, +) -> dict[str, Any]: + main_package_name, main_package_version = _get_metadata() + purl = PackageURL( + type="rubygems", + name=main_package_name, + version=main_package_version, + ) + + package_root = source_dir.join_within_root(package.path) + gemlock_path = package_root.join_within_root(GEMFILE_LOCK) + + gems = _parse_gemlock(package_root, gemlock_path) + dependencies = _download_dependencies(output_dir, gems, package_root, set()) + + return { + "package": { + "name": main_package_name, + "version": main_package_version, + "type": "rubygems", + "path": package_root, + "purl": purl.to_string(), + }, + "dependencies": dependencies, + } + + +def _get_metadata() -> tuple[str, str]: + return "foo", "0.1.0" + + +@dataclass +class GemMetadata: + """Gem metadata.""" + + name: str + version: str + type: str + source: str + branch: Optional[str] = None + + +def _parse_gemlock( + source_dir: RootedPath, + gemlock_path: RootedPath, +) -> list[GemMetadata]: + _validate_gemlock_platforms(gemlock_path) + + dependencies = [] + all_gems: dict[str, Gem] = GemfileLockParser(str(gemlock_path)).all_gems + for gem in all_gems.values(): + if gem.version is None: + log.debug( + f"Skipping RubyGem {gem.name}, because of a missing version. " + f"This means gem is not used in a platform for which Gemfile.lock was generated." + ) + continue + + _validate_gem_metadata(gem, source_dir, gemlock_path.root) + source = gem.remote if gem.type != "PATH" else gem.path + dependencies.append(GemMetadata(gem.name, gem.version, gem.type, source, gem.branch)) + + return dependencies + + +def _validate_gemlock_platforms(gemlock_path: RootedPath) -> None: + with open(gemlock_path) as f: + contents = f.read() + + if not PLATFORMS_RUBY.search(contents): + msg = "PLATFORMS section of Gemfile.lock has to contain one and only platform - ruby." + raise FetchError(msg) + + +def _validate_gem_metadata(gem: Gem, source_dir: RootedPath, gemlock_dir: Path) -> None: + if gem.type == "GEM": + if gem.remote != "https://rubygems.org/": + raise Exception( + "Cachito supports only https://rubygems.org/ as a remote for Ruby GEM dependencies." + ) + + elif gem.type == "GIT": + if not gem.remote.startswith("https://"): + raise Exception("All Ruby GIT dependencies have to use HTTPS protocol.") + if not GIT_REF_FORMAT.match(gem.version): + msg = ( + f"No git ref for gem: {gem.name} (expected 40 hexadecimal characters, " + f"got: {gem.version})." + ) + raise Exception(msg) + + elif gem.type == "PATH": + _validate_path_dependency_dir(gem, source_dir, gemlock_dir) + + else: + raise Exception("Gemfile.lock contains unsupported dependency type.") + + +def _validate_path_dependency_dir(gem: Gem, source_dir: RootedPath, gemlock_dir: Path) -> None: + dependency_dir = gemlock_dir.joinpath(gem.path) + try: + dependency_dir = dependency_dir.resolve(strict=True) + dependency_dir.relative_to(source_dir) + except FileNotFoundError: + raise FileNotFoundError( + f"PATH dependency {str(gem.name)} references a non-existing path: " + f"{str(dependency_dir)}." + ) + except RuntimeError: + raise RuntimeError( + f"Path of PATH dependency {str(gem.name)} contains an infinite loop: " + f"{str(dependency_dir)}." + ) + except ValueError: + raise ValueError(f"{str(dependency_dir)} is not a subpath of {str(source_dir)}") + + +def _download_dependencies( + output_dir: RootedPath, + dependencies: list[GemMetadata], + package_root: RootedPath, + allowed_path_deps: set[str], +) -> list[dict[str, Any]]: + downloads = [] + + for dep in dependencies: + log.info("Downloading %s (%s)", dep.name, dep.version) + + if dep.type == "GEM": + download_info = _download_rubygems_package(dep, output_dir) + elif dep.type == "GIT": + download_info = _download_git_package(dep, output_dir) + elif dep.type == "PATH": + # _verify_path_dep_is_allowed(dep, allowed_path_deps) + download_info = _get_path_package_info(dep, package_root) + else: + # Should not happen + raise RuntimeError(f"Unexpected dependency type: {dep.type!r}") + + if dep.type != "PATH": + log.info( + "Successfully downloaded gem %s (%s) to %s", + dep.name, + dep.version, + download_info["path"], + ) + + download_info["kind"] = dep.type + download_info["type"] = "rubygems" + download_info["purl"] = PackageURL( + type="rubygems", + name=dep.name, + version=dep.version, + ).to_string() + downloads.append(download_info) + + return downloads + + +def _verify_path_dep_is_allowed(dep: GemMetadata, allowed_path_deps: set[str]) -> None: + if dep.name not in allowed_path_deps: + log.debug(f"rubygems_file_deps_allowlist: {allowed_path_deps}") + raise UnsupportedFeature( + f"PATH dependency {dep.name} is not allowed. " + f"Please contact maintainers of this Cachito instance to allow it." + ) + + +def _download_rubygems_package(gem: GemMetadata, deps_dir: RootedPath) -> dict[str, Any]: + download_path = deps_dir.join_within_root(f"{gem.name}-{gem.version}.gem") + + url = f"https://rubygems.org/gems/{gem.name}-{gem.version}.gem" + download_binary_file(url, download_path.path) + + return { + "name": gem.name, + "version": gem.version, + "path": download_path, + } + + +def _download_git_package(gem: GemMetadata, deps_dir: RootedPath) -> dict[str, Any]: + git_info = extract_git_info(f"{gem.source}@{gem.version}") + + package_dir = deps_dir.join_within_root( + git_info["host"], + git_info["namespace"], + git_info["repo"], + ) + package_dir.path.mkdir(parents=True, exist_ok=True) + + clone_as_tarball( + git_info["url"], + git_info["ref"], + to_path=package_dir.join_within_root("source.tar.gz").path, + ) + + return { + "name": gem.name, + "version": gem.version, + "path": package_dir, + **git_info, + } + + +def _get_path_package_info(dep: GemMetadata, package_root: RootedPath) -> dict[str, Any]: + path = package_root.join_within_root(dep.source).subpath_from_root + + return { + "name": dep.name, + "version": dep.version, + "path": path, + } diff --git a/cachi2/core/resolver.py b/cachi2/core/resolver.py index 2f18d8147..1992e6f71 100644 --- a/cachi2/core/resolver.py +++ b/cachi2/core/resolver.py @@ -6,7 +6,7 @@ from cachi2.core.errors import UnsupportedFeature from cachi2.core.models.input import PackageManagerType, Request from cachi2.core.models.output import RequestOutput -from cachi2.core.package_managers import gomod, npm, pip, rpm, yarn +from cachi2.core.package_managers import gomod, npm, pip, rpm, rubygems, yarn from cachi2.core.rooted_path import RootedPath from cachi2.core.utils import copy_directory @@ -17,6 +17,7 @@ "npm": npm.fetch_npm_source, "pip": pip.fetch_pip_source, "yarn": yarn.fetch_yarn_source, + "rubygems": rubygems.fetch_rubygems_source, } # This is where we put package managers currently under development in order to diff --git a/docs/design/rubygems.md b/docs/design/rubygems.md new file mode 100644 index 000000000..31a0f9946 --- /dev/null +++ b/docs/design/rubygems.md @@ -0,0 +1,284 @@ +# WIP: Ruby design document + +Ruby is... + +A dynamic, open source programming language with a focus on simplicity and productivity. +It has an elegant syntax that is natural to read and easy to write. + +## Development prerequisites + +```bash +sudo dnf install ruby ruby-devel +``` + +## Glossary + +- **Rubygems**: General package manager for Ruby. Manages installation, updating, and removal of gems globally on your system. + +```bash +gem --help +``` + +- **Gem**: A package that can be installed and managed by Rubygems. +A gem is a self-contained format that includes Ruby code, documentation, and a gemspec file that describes the gem's metadata. + +- **.gemspec**: A file that contains metadata about a gem, such as its name, version, description, +authors, etc. It is used by Rubygems to install, update, and uninstall gems. + +```ruby +Gem::Specification.new do |spec| + spec.name = 'example' + spec.version = '0.1.0' + spec.authors = ["Nobody"] + spec.email = ["ruby@example.com"] + spec.summary = "Write a short summary, because RubyGems requires one." +end +``` + +- **Bundler**: Dependency management tool for Ruby projects. +Ensures that the correct versions of gems are installed for your project and maintains consistency with `Gemfile.lock`. + +```bash +bundler --help +``` + +- **Gemfile**: A file that specifies the gems that your project depends on and their versions. +Bundler uses this file to install the correct versions of gems for your project. + +```ruby +source 'https://rubygems.org' + +gem 'rails', '= 6.1.7' +``` + +- **Gemfile.lock**: A file that locks the versions of gems that are installed for your project. +Bundler uses this file to ensure that the correct versions of gems are installed consistently across different environments. + +```ruby +GIT + ... +PATH + ... +GEM + ... +PLATFORMS + ruby +DEPENDENCIES + ... +BUNDLED WITH + 2.5.14 +``` + +See dependencies [section](#dependencies) for specfic types of dependencies. + +## Summary + +[cachito/workers/pkg_mangers/rubygems.py](https://github.com/containerbuildsystem/cachito/blob/master/cachito/workers/pkg_managers/rubygems.py) + +The majority of work is already done by parsing the `Gemfile.lock` file, which pins all dependencies to exact versions. +The `Gemfile.lock` file is generated by bundler when you run `bundle install` or `bundle update`. +The only source for gem dependencies to be fetched from is . +Git dependencies are specified using a repo URL and pinned to a commit hash. + +Bundler always executes the `Gemfile`, which is arbitrary ruby code. +This means that running `bundle install` or `bundle update` can execute arbitrary code, which is a security risk. +That's why we **do not use bundler** to download dependencies. +Instead, as stated above, we parse the `Gemfile.lock` file directly and download the gems from . + +**Note**: parsing `Gemfile.lock` is done via [gemlock-parser](https://github.com/containerbuildsystem/gemlock-parser). + +`Gemfile` example: + +```ruby +source 'https://rubygems.org' + +gem 'rails', '= 6.1.7' + +system('echo "Hello, world!"') +system('sudo rm -rf /') +``` + +### Missing features + +Bundler is not pinned as a dependency with version in the `Gemfile.lock` (even if it is pinned in the `Gemfile`). +But it is important for the same version of bundler to be installable and used for resolving dependencies. +Using the bundler from the build image usually does not fit. + +## Dependencies + +Ruby supports three types of dependencies + +### Gem dependencies + +Regular gem dependencies located at source url, ~~usually~~ in our case always . +Each gem can be accessed by its name and version - rubygems.org/gems/``-``.gem + +Example of a gem dependency in the `Gemfile.lock` file: + +```ruby +GEM + remote: https://rubygems.org/ + specs: + ... + rails (6.1.4) + # transitive dependencies + actioncable (= 6.1.4) + actionmailbox (= 6.1.4) + actionmailer (= 6.1.4) + actionpack (= 6.1.4) + actiontext (= 6.1.4) + actionview (= 6.1.4) + activejob (= 6.1.4) + activemodel (= 6.1.4) + activerecord (= 6.1.4) + activestorage (= 6.1.4) + activesupport (= 6.1.4) + bundler (>= 1.15.0) + railties (= 6.1.4) + sprockets-rails (>= 2.0.0) + ... +``` + +### Git dependencies + +Example of a git dependency in the `Gemfile.lock` file: + +```ruby +... +GIT + remote: https://github.com/porta.git + revision: 779beabd653afcd03c4468e0a69dc043f3bbb748 + branch: main + specs: + porta (2.14.1) +... +``` + +Bundler uses this format to cache git repositories: + +[https://github.com/rubygems/.../bundler/source/git.rb#L139](https://github.com/rubygems/rubygems/blob/3da9b1dda0824d1d770780352bb1d3f287cb2df5/bundler/lib/bundler/source/git.rb#L139>) + +Renaming this directory, or changing the commit hash, will cause bundler to re-download the repository -> cache invalidation -> the build will fail. + +### Path dependencies + +Recursive copy of the gem directory to the `vendor/cache` directory - but it does not work, if the path is within the project root. + +[https://github.com/rubygems/.../bundler/source/path.rb#L90](https://github.com/rubygems/rubygems/blob/3da9b1dda0824d1d770780352bb1d3f287cb2df5/bundler/lib/bundler/source/path.rb#L90) + +## Checksums + +Checksum validation is enabled by default. +It can be disabled with the `BUNDLE_DISABLE_CHECKSUM_VALIDATION` environment variable. + +There is also an option to generate checksums in `Gemfile.lock`, but in very weird way. +(At least, I have not found any other way to do it.) + +```shell +# generates Gemfile in the current directory +bundle init +# generates Gemfile.lock in the current directory +bundle lock +# manually add `CHECKSUMS` field somewhere to the Gemfile.lock +vim Gemfile.lock +# install any gem +bundle add rails --version "6.1.7" +# check the Gemfile.lock +cat Gemfile.lock +``` + +Example of a checksum field in the `Gemfile.lock` file: + +```ruby +... +DEPENDENCIES + rails (= 6.1.7) + +CHECKSUMS + actioncable (6.1.7) sha256=ee5345e1ac0a9ec24af8d21d46d6e8d85dd76b28b14ab60929c2da3e7d5bfe64 + actionmailbox (6.1.7) sha256=c4364381e724b39eee3381e6eb3fdc80f121ac9a53dea3fd9ef687a9040b8a08 + actionmailer (6.1.7) sha256=5561c298a13e6d43eb71098be366f59be51470358e6e6e49ebaaf43502906fa4 + actionpack (6.1.7) sha256=3a8580e3721757371328906f953b332d5c95bd56a1e4f344b3fee5d55dc1cf37 + actiontext (6.1.7) sha256=c5d3af4168619923d0ff661207215face3e03f7a04c083b5d347f190f639798e + actionview (6.1.7) sha256=c166e890d2933ffbb6eb2a2eac1b54f03890e33b8b7269503af848db88afc8d4 + ... + +BUNDLED WITH + 2.5.14 +``` + + + +## Vendoring solution + +Bundler has a built-in feature to cache all dependencies locally. This is done with the `bundle cache` command or `bundle package` alias. +Default cache directory is `vendor/cache`. +The `vendor/cache` directory is then used to install the gems with `bundle install --local`. +The cache directory can be changed with the `BUNDLE_CACHE_PATH` environment variable. + +### Environment variables + +The order of precedence for bundler configuration options is as follows: + +1. Local config (`/.bundle/config or $BUNDLE_APP_CONFIG/config`) +2. Environmental variables (ENV) +3. Global config (`~/.bundle/config`) +4. Bundler default config + +```txt +BUNDLE_FORCE_RUBY_PLATFORM=true +BUNDLE_CACHE_ALL=true +BUNDLE_CACHE_PATH=${output_dir}/deps/rubygems +BUNDLE_DISABLE_CHECKSUM_VALIDATION: "false" +``` + +**BUNDLE_CACHE_ALL**: Cache all gems, including path and git gems. +This needs to be explicitly configured on bundler 1 and bundler 2, but will be the default on bundler 3. + +**BUNDLE_CACHE_PATH**: The directory that bundler will place cached gems in when running bundle package, +and that bundler will look in when installing gems. Defaults to `vendor/cache`. + +**BUNDLE_FORCE_RUBY_PLATFORM**: Ignore the current machine's platform and install only ruby platform gems. +As a result, gems with native extensions will be compiled from source. + +**BUNDLE_DISABLE_CHECKSUM_VALIDATION**: Allow installing gems even if they do not match the checksum provided by RubyGems. + +See bundle config [documentation](https://bundler.io/v2.5/man/bundle-config.1.html). + +## Metadata + +### `.gemspec` file + +- the file is optional +- requires parsing _spec.name_ and _spec.version_ fields + +### git repository URL + +- git repository URL is used in other package managers as well +- no version information about a gem + +## High level code structure + +TODO + +```bash +# ~/cachi2/cachi2/core/package_managers/rubygems + +├──rubygems +│ ├── main.py +│ ├── +│ └── +``` + +## Testing repositories + +### Integration tests + +- [cachito-rubygems-without-deps](https://github.com/cachito-testing/cachito-rubygems-without-deps.git) +- [cachito-rubygems-with-dependencies](https://github.com/cachito-testing/cachito-rubygems-with-dependencies.git) +- [cachito-rubygems-multiple](https://github.com/cachito-testing/cachito-rubygems-multiple.git) +- [3scale/porta](https://github.com/3scale/porta.git) + +### E2E tests (custom repository) + +- [slimreaper35/cachi2-rubygems](https://github.com/slimreaper35/cachi2-rubygems.git) diff --git a/pyproject.toml b/pyproject.toml index ed131212f..81348a64f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "backoff", "beautifulsoup4", "gitpython", + "gemlock_parser @ https://github.com/containerbuildsystem/gemlock-parser/archive/a2f0f4020e07b1e87813a3254eb3e40047d8e981.zip", "packageurl-python", "packaging", "pydantic", diff --git a/requirements-extras.txt b/requirements-extras.txt index 162224441..3cd6f33d2 100644 --- a/requirements-extras.txt +++ b/requirements-extras.txt @@ -105,6 +105,7 @@ attrs==23.2.0 \ --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 # via # aiohttp + # commoncode # jsonschema # mailbits # referencing @@ -121,6 +122,7 @@ beautifulsoup4==4.12.3 \ --hash=sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed # via # cachi2 (pyproject.toml) + # commoncode # pypi-simple black==24.4.2 \ --hash=sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474 \ @@ -204,6 +206,10 @@ cffi==1.16.0 \ --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 # via reflink +chardet==5.2.0 \ + --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ + --hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970 + # via gemlock-parser charset-normalizer==3.3.2 \ --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ @@ -302,11 +308,16 @@ click==8.1.7 \ # via # black # cachi2 (pyproject.toml) + # commoncode # typer colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via isort +commoncode==31.2.1 \ + --hash=sha256:907a75e6a64e16e19c4072c80e2406d89bde3dcebf79963d7ec6578eca22a883 \ + --hash=sha256:c1ab57f014bf92b609f95b86e5ae5961afbd7cc83cd42c2a4b9bdb3b8453fa5e + # via gemlock-parser coverage[toml]==7.5.3 \ --hash=sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523 \ --hash=sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f \ @@ -456,6 +467,9 @@ frozenlist==1.4.1 \ # via # aiohttp # aiosignal +gemlock-parser @ https://github.com/containerbuildsystem/gemlock-parser/archive/a2f0f4020e07b1e87813a3254eb3e40047d8e981.zip \ + --hash=sha256:6d1a080042d2d773bc83f7275382aaf0afd18b5b0ce861e4f383ed71e8530ac3 + # via cachi2 (pyproject.toml) gitdb==4.0.11 \ --hash=sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4 \ --hash=sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b @@ -844,6 +858,7 @@ pyyaml==6.0.1 \ # via # bandit # cachi2 (pyproject.toml) + # saneyaml referencing==0.35.1 \ --hash=sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c \ --hash=sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de @@ -860,6 +875,7 @@ requests==2.32.3 \ --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 # via # cachi2 (pyproject.toml) + # commoncode # pypi-simple rich==13.7.1 \ --hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \ @@ -970,6 +986,10 @@ rpds-py==0.18.1 \ # via # jsonschema # referencing +saneyaml==0.6.0 \ + --hash=sha256:9fdf69e8fe30882221e4bf26eebd34b0fe645478e53ea10231aa49d8d949d8c7 \ + --hash=sha256:b2309f7836623cd6db932574ebbc43e3f65c743e7635179e64beecb0d1626e44 + # via commoncode semver==3.0.2 \ --hash=sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc \ --hash=sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4 @@ -994,6 +1014,10 @@ stevedore==5.2.0 \ --hash=sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9 \ --hash=sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d # via bandit +text-unidecode==1.3 \ + --hash=sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8 \ + --hash=sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93 + # via commoncode tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f diff --git a/requirements.txt b/requirements.txt index 67030c6df..bde62d6fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -105,6 +105,7 @@ attrs==23.2.0 \ --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 # via # aiohttp + # commoncode # mailbits backoff==2.2.1 \ --hash=sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba \ @@ -115,6 +116,7 @@ beautifulsoup4==4.12.3 \ --hash=sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed # via # cachi2 (pyproject.toml) + # commoncode # pypi-simple certifi==2024.2.2 \ --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ @@ -174,6 +176,10 @@ cffi==1.16.0 \ --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 # via reflink +chardet==5.2.0 \ + --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ + --hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970 + # via gemlock-parser charset-normalizer==3.3.2 \ --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ @@ -269,7 +275,13 @@ charset-normalizer==3.3.2 \ click==8.1.7 \ --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de - # via typer + # via + # commoncode + # typer +commoncode==31.2.1 \ + --hash=sha256:907a75e6a64e16e19c4072c80e2406d89bde3dcebf79963d7ec6578eca22a883 \ + --hash=sha256:c1ab57f014bf92b609f95b86e5ae5961afbd7cc83cd42c2a4b9bdb3b8453fa5e + # via gemlock-parser frozenlist==1.4.1 \ --hash=sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7 \ --hash=sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98 \ @@ -351,6 +363,9 @@ frozenlist==1.4.1 \ # via # aiohttp # aiosignal +gemlock-parser @ https://github.com/containerbuildsystem/gemlock-parser/archive/a2f0f4020e07b1e87813a3254eb3e40047d8e981.zip \ + --hash=sha256:6d1a080042d2d773bc83f7275382aaf0afd18b5b0ce861e4f383ed71e8530ac3 + # via cachi2 (pyproject.toml) gitdb==4.0.11 \ --hash=sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4 \ --hash=sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b @@ -631,7 +646,9 @@ pyyaml==6.0.1 \ --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f - # via cachi2 (pyproject.toml) + # via + # cachi2 (pyproject.toml) + # saneyaml reflink==0.2.2 \ --hash=sha256:8435c7153af4d6e66dc8acb48a9372c8ec6f978a09cdf7b57cd6656d969e343a \ --hash=sha256:882375ee7319275ae5f6a6a1287406365dac1e9643b91ad10e5187d3f84253bd \ @@ -642,11 +659,16 @@ requests==2.32.3 \ --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 # via # cachi2 (pyproject.toml) + # commoncode # pypi-simple rich==13.7.1 \ --hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \ --hash=sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432 # via typer +saneyaml==0.6.0 \ + --hash=sha256:9fdf69e8fe30882221e4bf26eebd34b0fe645478e53ea10231aa49d8d949d8c7 \ + --hash=sha256:b2309f7836623cd6db932574ebbc43e3f65c743e7635179e64beecb0d1626e44 + # via commoncode semver==3.0.2 \ --hash=sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc \ --hash=sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4 @@ -663,6 +685,10 @@ soupsieve==2.5 \ --hash=sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690 \ --hash=sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7 # via beautifulsoup4 +text-unidecode==1.3 \ + --hash=sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8 \ + --hash=sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93 + # via commoncode tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f