├── .bazelignore ├── .bazelrc ├── .bazelversion ├── .bcr ├── README.md ├── config.yml ├── metadata.template.json ├── presubmit.yml └── source.template.json ├── .gitattributes ├── .github └── workflows │ ├── BUILD.bazel │ ├── buildifier.yaml │ ├── ci.bazelrc │ ├── ci.yaml │ ├── release.yml │ └── release_prep.sh ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── BUILD.bazel ├── CONTRIBUTING.md ├── LICENSE ├── MODULE.bazel ├── README.md ├── SECURITY.md ├── WORKSPACE ├── apt ├── BUILD.bazel ├── apt.bzl ├── defs.bzl ├── extensions.bzl ├── private │ ├── BUILD.bazel │ ├── apt_deb_repository.bzl │ ├── apt_dep_resolver.bzl │ ├── copy.sh.tmpl │ ├── deb_import.bzl │ ├── deb_postfix.bzl │ ├── deb_resolve.bzl │ ├── deb_translate_lock.bzl │ ├── dpkg_status.bzl │ ├── dpkg_status.sh │ ├── dpkg_statusd.bzl │ ├── dpkg_statusd.sh │ ├── lockfile.bzl │ ├── package.BUILD.tmpl │ ├── starlark_codegen_utils.bzl │ ├── util.bzl │ ├── version.bzl │ └── version_constraint.bzl └── tests │ ├── BUILD.bazel │ ├── resolution │ ├── BUILD.bazel │ ├── arch_all.yaml │ ├── clang.yaml │ └── security.yaml │ ├── resolution_test.bzl │ └── version_test.bzl ├── distroless ├── BUILD.bazel ├── defs.bzl ├── dependencies.bzl ├── private │ ├── BUILD.bazel │ ├── JavaKeyStore.java │ ├── cacerts.bzl │ ├── cacerts.sh │ ├── flatten.bzl │ ├── flatten.sh │ ├── group.bzl │ ├── home.bzl │ ├── java_keystore.bzl │ ├── locale.bzl │ ├── locale.sh │ ├── os_release.bzl │ ├── passwd.bzl │ ├── tar.bzl │ └── util.bzl ├── tests │ ├── BUILD.bazel │ └── asserts.bzl └── toolchains.bzl ├── docs ├── .bazelrc ├── .bazelversion ├── BUILD.bazel ├── MODULE.bazel ├── apt.md ├── apt_macro.md └── rules.md └── e2e └── smoke ├── .bazelversion ├── BUILD ├── MODULE.bazel ├── README.md ├── bullseye.lock.json ├── bullseye.yaml ├── test_linux_amd64.yaml └── test_linux_arm64.yaml /.bazelignore: -------------------------------------------------------------------------------- 1 | # nested modules 2 | docs/ 3 | e2e/ 4 | -------------------------------------------------------------------------------- /.bazelrc: -------------------------------------------------------------------------------- 1 | # Bazel settings that apply to this repository. 2 | # Take care to document any settings that you expect users to apply. 3 | # Settings that apply only to CI are in .github/workflows/ci.bazelrc 4 | 5 | # Required until this is the default; expected in Bazel 7 6 | common --enable_bzlmod 7 | 8 | # Don’t want to push a rules author to update their deps if not needed. 9 | # https://bazel.build/reference/command-line-reference#flag--check_direct_dependencies 10 | # https://bazelbuild.slack.com/archives/C014RARENH0/p1691158021917459?thread_ts=1691156601.420349&cid=C014RARENH0 11 | common --check_direct_dependencies=off 12 | 13 | # Enable platform specific options 14 | build --enable_platform_specific_config 15 | 16 | # Use a hermetic Java version 17 | build --java_runtime_version=remotejdk_11 18 | 19 | # Newer versions jdk creates collisions on /tmp 20 | # See: https://github.com/bazelbuild/bazel/issues/3236 21 | # https://github.com/GoogleContainerTools/rules_distroless/actions/runs/7118944984/job/19382981899?pr=9#step:8:51 22 | common:linux --sandbox_tmpfs_path=/tmp 23 | 24 | 25 | # Allow external dependencies to be retried. debian snapshot is unreliable and needs retries. 26 | common --experimental_repository_downloader_retries=10 27 | 28 | # Unflip this flag for now, it fails on Bazel 8 29 | common --noincompatible_disallow_empty_glob 30 | 31 | # Load any settings specific to the current user. 32 | # .bazelrc.user should appear in .gitignore so that settings are not shared with team members 33 | # This needs to be last statement in this 34 | # config, as the user configuration should be able to overwrite flags from this file. 35 | # See https://docs.bazel.build/versions/master/best-practices.html#bazelrc 36 | # (Note that we use .bazelrc.user so the file appears next to .bazelrc in directory listing, 37 | # rather than user.bazelrc as suggested in the Bazel docs) 38 | try-import %workspace%/.bazelrc.user 39 | -------------------------------------------------------------------------------- /.bazelversion: -------------------------------------------------------------------------------- 1 | 7.0.0 2 | # The first line of this file is used by Bazelisk and Bazel to be sure 3 | # the right version of Bazel is used to build and test this repo. 4 | # This also defines which version is used on CI. 5 | # 6 | # Note that you should also run integration_tests against other Bazel 7 | # versions you support. 8 | -------------------------------------------------------------------------------- /.bcr/README.md: -------------------------------------------------------------------------------- 1 | # Bazel Central Registry 2 | 3 | When the ruleset is released, we want it to be published to the 4 | Bazel Central Registry automatically: 5 | 6 | 7 | This folder contains configuration files to automate the publish step. 8 | See 9 | for authoritative documentation about these files. 10 | -------------------------------------------------------------------------------- /.bcr/config.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/bazel-contrib/publish-to-bcr#a-note-on-release-automation 2 | # for guidance about whether to uncomment this section: 3 | # 4 | # fixedReleaser: 5 | # login: loosebazooka 6 | # email: appu@google.com 7 | -------------------------------------------------------------------------------- /.bcr/metadata.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://github.com/GoogleContainerTools/rules_distroless", 3 | "maintainers": [ 4 | { 5 | "email": "appu@google.com", 6 | "github": "loosebazooka", 7 | "name": "Appu Goundan" 8 | }, 9 | { 10 | "email": "sahin@aspect.dev", 11 | "github": "thesayyn", 12 | "name": "Şahin Yort" 13 | } 14 | ], 15 | "repository": ["github:GoogleContainerTools/rules_distroless"], 16 | "versions": [], 17 | "yanked_versions": {} 18 | } 19 | -------------------------------------------------------------------------------- /.bcr/presubmit.yml: -------------------------------------------------------------------------------- 1 | bcr_test_module: 2 | module_path: "e2e/smoke" 3 | matrix: 4 | platform: ["debian10", "macos", "ubuntu2004"] 5 | bazel: [7.x, 8.x] 6 | tasks: 7 | run_tests: 8 | name: "Run test module" 9 | bazel: ${{ bazel }} 10 | platform: ${{ platform }} 11 | test_targets: 12 | - "//..." 13 | -------------------------------------------------------------------------------- /.bcr/source.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrity": "**leave this alone**", 3 | "strip_prefix": "{REPO}-{VERSION}", 4 | "url": "https://github.com/{OWNER}/{REPO}/releases/download/{TAG}/rules_distroless-{TAG}.tar.gz" 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # In code review, collapse generated files 2 | docs/*.md linguist-generated=true 3 | 4 | ################################# 5 | # Configuration for 'git archive' 6 | # See https://git-scm.com/docs/git-archive#ATTRIBUTES 7 | 8 | # Don't include examples in the distribution artifact, to reduce size. 9 | # You may want to add additional exclusions for folders or files that users don't need. 10 | examples export-ignore 11 | 12 | # Occasionally there's a need to "stamp" the release version into a file 13 | distroless/version.bzl export-subst 14 | -------------------------------------------------------------------------------- /.github/workflows/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@buildifier_prebuilt//:rules.bzl", "buildifier") 2 | 3 | buildifier( 4 | name = "buildifier.check", 5 | exclude_patterns = ["./.git/*"], 6 | lint_mode = "warn", 7 | mode = "diff", 8 | ) 9 | -------------------------------------------------------------------------------- /.github/workflows/buildifier.yaml: -------------------------------------------------------------------------------- 1 | name: Buildifier 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | jobs: 14 | check: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: buildifier 19 | run: bazel run --enable_bzlmod //.github/workflows:buildifier.check 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.bazelrc: -------------------------------------------------------------------------------- 1 | # This file contains Bazel settings to apply on CI only. 2 | # It is referenced with a --bazelrc option in the call to bazel in ci.yaml 3 | 4 | # Debug where options came from 5 | build --announce_rc 6 | # This directory is configured in GitHub actions to be persisted between runs. 7 | # We do not enable the repository cache to cache downloaded external artifacts 8 | # as these are generally faster to download again than to fetch them from the 9 | # GitHub actions cache. 10 | build --disk_cache=~/.cache/bazel 11 | # Don't rely on test logs being easily accessible from the test runner, 12 | # though it makes the log noisier. 13 | test --test_output=errors 14 | # Allows tests to run bazelisk-in-bazel, since this is the cache folder used 15 | test --test_env=XDG_CACHE_HOME 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | concurrency: 14 | # Cancel previous actions from the same PR: https://stackoverflow.com/a/72408109 15 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | test: 20 | uses: bazel-contrib/.github/.github/workflows/bazel.yaml@d8163053334bda95e01b01348d218441132276b2 21 | with: 22 | folders: | 23 | [ 24 | ".", 25 | "docs", 26 | "e2e/smoke" 27 | ] 28 | exclude: | 29 | [ 30 | {"folder": ".", "bzlmodEnabled": false}, 31 | {"folder": "docs", "bzlmodEnabled": false}, 32 | {"folder": "docs", "bazelVersion": "7.0.0"}, 33 | {"folder": "e2e/smoke", "bzlmodEnabled": false}, 34 | {"os": "windows-latest"}, 35 | ] 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Cut a release whenever a new tag is pushed to the repo. 2 | # You should use an annotated tag, like `git tag -a v1.2.3` 3 | # and put the release notes into the commit message for the tag. 4 | name: Release 5 | 6 | on: 7 | push: 8 | tags: 9 | - "v*.*.*" 10 | 11 | jobs: 12 | release: 13 | uses: bazel-contrib/.github/.github/workflows/release_ruleset.yaml@c09f979eb364df0c5a4bbf954d964217f2cae3be 14 | with: 15 | release_files: rules_distroless-*.tar.gz 16 | -------------------------------------------------------------------------------- /.github/workflows/release_prep.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit -o nounset -o pipefail 4 | 5 | # Set by GH actions, see 6 | # https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables 7 | TAG=${GITHUB_REF_NAME} 8 | # The prefix is chosen to match what GitHub generates for source archives 9 | # This guarantees that users can easily switch from a released artifact to a source archive 10 | # with minimal differences in their code (e.g. strip_prefix remains the same) 11 | PREFIX="rules_distroless-${TAG:1}" 12 | ARCHIVE="rules_distroless-$TAG.tar.gz" 13 | 14 | # NB: configuration for 'git archive' is in /.gitattributes 15 | git archive --format=tar --prefix=${PREFIX}/ ${TAG} | gzip > $ARCHIVE 16 | SHA=$(shasum -a 256 $ARCHIVE | awk '{print $1}') 17 | 18 | cat << EOF 19 | ## Using BZLMOD 20 | 21 | 1. Enable with \`common --enable_bzlmod\` in \`.bazelrc\`. 22 | 2. Add to your \`MODULE.bazel\` file: 23 | 24 | \`\`\`starlark 25 | bazel_dep(name = "rules_distroless", version = "${TAG:1}") 26 | \`\`\` 27 | 28 | ## Using WORKSPACE 29 | 30 | We do not support WORKSPACE. Use Bzlmod instead. 31 | EOF 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignoring MODULE.bazel.lock for the time being 2 | # see https://github.com/bazelbuild/bazel/issues/20369 3 | MODULE.bazel.lock 4 | e2e/smoke/MODULE.bazel.lock 5 | bazel-* 6 | .bazelrc.user 7 | .idea/ 8 | .ijwb/ 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See CONTRIBUTING.md for instructions. 2 | # See https://pre-commit.com for more information 3 | # See https://pre-commit.com/hooks.html for more hooks 4 | 5 | # Commitizen runs in commit-msg stage 6 | # but we don't want to run the other hooks on commit messages 7 | default_stages: [pre-commit] 8 | 9 | # Use a slightly older version of node by default 10 | # as the default uses a very new version of GLIBC 11 | default_language_version: 12 | node: 16.18.0 13 | 14 | repos: 15 | # Check formatting and lint for starlark code 16 | - repo: https://github.com/keith/pre-commit-buildifier 17 | rev: 7.3.1.1 18 | hooks: 19 | - id: buildifier 20 | - id: buildifier-lint 21 | # Enforce that commit messages allow for later changelog generation 22 | - repo: https://github.com/commitizen-tools/commitizen 23 | rev: v4.1.0 24 | hooks: 25 | # Requires that commitizen is already installed 26 | - id: commitizen 27 | stages: [commit-msg] 28 | - repo: https://github.com/pre-commit/mirrors-prettier 29 | rev: v3.1.0 30 | hooks: 31 | - id: prettier 32 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | docs/*.md 2 | *.lock.json -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_gazelle//:def.bzl", "gazelle", "gazelle_binary") 2 | 3 | gazelle_binary( 4 | name = "gazelle_bin", 5 | languages = ["@bazel_skylib_gazelle_plugin//bzl"], 6 | ) 7 | 8 | gazelle( 9 | name = "gazelle", 10 | gazelle = "gazelle_bin", 11 | ) 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code Reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MODULE.bazel: -------------------------------------------------------------------------------- 1 | "Bazel dependencies" 2 | 3 | module( 4 | name = "rules_distroless", 5 | version = "0.0.0", 6 | compatibility_level = 1, 7 | ) 8 | 9 | bazel_dep(name = "platforms", version = "0.0.10") 10 | bazel_dep(name = "bazel_features", version = "1.20.0") 11 | bazel_dep(name = "bazel_skylib", version = "1.5.0") 12 | bazel_dep(name = "aspect_bazel_lib", version = "2.14.0") 13 | bazel_dep(name = "rules_java", version = "8.8.0") 14 | bazel_dep(name = "rules_shell", version = "0.4.1") 15 | 16 | bazel_lib_toolchains = use_extension("@aspect_bazel_lib//lib:extensions.bzl", "toolchains") 17 | use_repo(bazel_lib_toolchains, "zstd_toolchains") 18 | use_repo(bazel_lib_toolchains, "bsd_tar_toolchains") 19 | use_repo(bazel_lib_toolchains, "yq_darwin_amd64") 20 | use_repo(bazel_lib_toolchains, "yq_darwin_arm64") 21 | use_repo(bazel_lib_toolchains, "yq_linux_amd64") 22 | use_repo(bazel_lib_toolchains, "yq_linux_arm64") 23 | use_repo(bazel_lib_toolchains, "yq_linux_ppc64le") 24 | use_repo(bazel_lib_toolchains, "yq_linux_s390x") 25 | use_repo(bazel_lib_toolchains, "yq_windows_amd64") 26 | 27 | # Dev dependencies 28 | bazel_dep(name = "gazelle", version = "0.34.0", dev_dependency = True, repo_name = "bazel_gazelle") 29 | bazel_dep(name = "bazel_skylib_gazelle_plugin", version = "1.5.0", dev_dependency = True) 30 | bazel_dep(name = "buildifier_prebuilt", version = "8.0.1", dev_dependency = True) 31 | bazel_dep(name = "rules_oci", version = "2.0.0", dev_dependency = True) 32 | bazel_dep(name = "container_structure_test", version = "1.16.0", dev_dependency = True) 33 | 34 | http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 35 | 36 | http_archive( 37 | name = "example-bullseye-ca-certificates", 38 | build_file_content = 'exports_files(["data.tar.xz", "control.tar.xz"])', 39 | sha256 = "b2d488ad4d8d8adb3ba319fc9cb2cf9909fc42cb82ad239a26c570a2e749c389", 40 | urls = ["https://snapshot.debian.org/archive/debian/20231106T210201Z/pool/main/c/ca-certificates/ca-certificates_20210119_all.deb"], 41 | ) 42 | 43 | http_archive( 44 | name = "example-bullseye-libc-bin", 45 | build_file_content = 'exports_files(["data.tar.xz"])', 46 | sha256 = "8b048ab5c7e9f5b7444655541230e689631fd9855c384e8c4a802586d9bbc65a", 47 | urls = ["https://snapshot.debian.org/archive/debian-security/20231106T230332Z/pool/updates/main/g/glibc/libc-bin_2.31-13+deb11u7_amd64.deb"], 48 | ) 49 | 50 | http_archive( 51 | name = "example-bookworm-libc-bin", 52 | build_file_content = 'exports_files(["data.tar.xz"])', 53 | sha256 = "38c44247c5b3e864d6db2877edd9c9a0555fc4e23ae271b73d7f527802616df5", 54 | urls = ["https://snapshot.debian.org/archive/debian-security/20231106T230332Z/pool/updates/main/g/glibc/libc-bin_2.36-9+deb12u3_armhf.deb"], 55 | ) 56 | 57 | apt = use_extension( 58 | "@rules_distroless//apt:extensions.bzl", 59 | "apt", 60 | dev_dependency = True, 61 | ) 62 | apt.install( 63 | name = "bullseye", 64 | lock = "//examples/debian_snapshot:bullseye.lock.json", 65 | manifest = "//examples/debian_snapshot:bullseye.yaml", 66 | ) 67 | apt.install( 68 | name = "bullseye_nolock", 69 | manifest = "//examples/debian_snapshot:bullseye.yaml", 70 | nolock = True, 71 | ) 72 | apt.install( 73 | name = "noble", 74 | lock = "//examples/ubuntu_snapshot:noble.lock.json", 75 | manifest = "//examples/ubuntu_snapshot:noble.yaml", 76 | ) 77 | apt.install( 78 | name = "resolution_test", 79 | manifest = "apt/tests/resolution/security.yaml", 80 | nolock = True, 81 | ) 82 | apt.install( 83 | name = "arch_all_test", 84 | manifest = "apt/tests/resolution/arch_all.yaml", 85 | nolock = True, 86 | ) 87 | apt.install( 88 | name = "clang", 89 | manifest = "apt/tests/resolution/clang.yaml", 90 | nolock = True, 91 | ) 92 | use_repo(apt, "arch_all_test", "arch_all_test_resolve", "bullseye", "bullseye_nolock", "clang", "noble", "resolution_test", "resolution_test_resolve") 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `rules_distroless` 2 | 3 | Bazel helper rules to aid with some of the steps needed to create a Linux / 4 | Debian installation. These rules are designed to replace commands such as 5 | `apt-get install`, `passwd`, `groupadd`, `useradd`, `update-ca-certificates`. 6 | 7 | > [!CAUTION] 8 | > 9 | > `rules_distroless` is currently in beta and does not yet offer a stable 10 | > Public API. However, many users are already successfully using it in 11 | > production environments. Check [Adopters](#adopters) to see who's already 12 | > using it. 13 | 14 | # Contributing 15 | 16 | This ruleset is primarily funded to support [Google's `distroless` container 17 | images]. We may not work on feature requests that do not support this mission. 18 | 19 | We will however accept fully tested contributions via pull requests if they 20 | align with the project goals (e.g. add support for a different compression 21 | format) and may reject requests that do not (e.g. supporting other packaging 22 | formats other than `.deb`). 23 | 24 | > [!TIP] 25 | > There's limited maintainer time for this project, so we strongly encourage 26 | > focused, small, and readable Pull Requests. 27 | 28 | # Usage 29 | 30 | ## Bzlmod (Bazel 6+) 31 | 32 | > [!NOTE] 33 | > If you are using Bazel 6 you need to enable Bzlmod by adding 34 | > `common --enable_bzlmod` to `.bazelrc` If you are using Bazel 7+ 35 | > [it's enabled by default]. 36 | 37 | Add the following to your `MODULE.bazel` file: 38 | 39 | ```starlark 40 | bazel_dep(name = "rules_distroless", version = "0.3.9") 41 | ``` 42 | 43 | You can find the latest release version in the [Bazel Central Registry]. 44 | 45 | If you want to use a specific commit (e.g. there are commits in `main` that are 46 | still not part of a release) you can use one of the few mechanisms that Bazel 47 | provides to override repos. 48 | 49 | You can use [`git_override`], [`archive_override`], etc (or 50 | [`local_path_override`] if you want to test a local patch): 51 | 52 | ```starlark 53 | bazel_dep(name = "rules_distroless", version = "0.3.9") 54 | 55 | git_override( 56 | module_name = "rules_distroless", 57 | remote = "https://github.com/GoogleContainerTools/rules_distroless.git", 58 | commit = "6ccc0307f618e67a9252bc6ce2112313c2c42b7f", 59 | ) 60 | ``` 61 | 62 | ## `WORKSPACE` (legacy) 63 | 64 | > [!WARNING] 65 | > Bzlmod is replacing the legacy `WORKSPACE` system. The `WORKSPACE` file will 66 | > be disabled by default in Bazel 8 (late 2024) and will be completely removed 67 | > in Bazel 9 (late 2025). Please migrate to Bzlmod following the steps in the 68 | > [Bzlmod migration guide]. 69 | 70 | You can find the latest release in the [`rules_distroless` Github releases 71 | page]. 72 | 73 | # Examples 74 | 75 | The [examples](/examples) demonstrate how to accomplish typical tasks such as 76 | **create a new user group** or **create a new home directory**: 77 | 78 | - [groupadd](/examples/group) 79 | - [passwd](/examples/passwd) 80 | - [useradd --home](/examples/home) 81 | - [update-ca-certificates](/examples/cacerts) 82 | - [keytool](/examples/java_keystore) 83 | - [apt-get install](/examples/debian_snapshot) from Debian repositories. 84 | - [apt-get install](/examples/ubuntu_snapshot) from Ubuntu repositories. 85 | 86 | We also have `distroless`-specific rules that could be useful: 87 | 88 | - [flatten](/examples/flatten): flatten multiple `tar` archives. 89 | - [os_release](/examples/os_release): create an `/etc/os-release` file. 90 | - [locale](/examples/locale): strip `/usr/lib/locale` to be smaller. 91 | - [dpkg_statusd](/examples/statusd): creates a `/var/lib/dpkg/status.d` 92 | package database for scanners to discover installed packages. 93 | 94 | # Public API Docs 95 | 96 | To read more specific documentation for each of the rules in the repo please 97 | check the following docs: 98 | 99 | - [apt](/docs/apt.md): repository rule for installing Debian/Ubuntu packages. 100 | - [apt macro](/docs/apt_macro.md): legacy macro for installing Debian/Ubuntu 101 | packages. 102 | - [rules](/docs/rules.md): various helper rules to aid with creating a Linux / 103 | Debian installation from scratch. 104 | 105 | # Adopters 106 | 107 | - [Google's `distroless` container images] 108 | - [Arize AI](https://www.arize.com) 109 | 110 | > [!TIP] 111 | > Are you using `rules_distroless`? Please send us a Pull Request to add your 112 | > project or company name here! 113 | 114 | [it's enabled by default]: https://blog.bazel.build/2023/12/11/bazel-7-release.html#bzlmod 115 | [Bazel Central Registry]: https://registry.bazel.build/modules/rules_distroless 116 | [`git_override`]: https://bazel.build/versions/6.0.0/rules/lib/globals#git_override 117 | [`archive_override`]: https://bazel.build/versions/6.0.0/rules/lib/globals#archive_override 118 | [`local_path_override`]: https://bazel.build/versions/6.0.0/rules/lib/globals#local_path_override 119 | [Bzlmod migration guide]: https://bazel.build/external/migration 120 | [`rules_distroless` Github releases page]: https://github.com/GoogleContainerTools/rules_distroless/releases 121 | [Update on the future stability of source code archives and hashes]: https://github.blog/2023-02-21-update-on-the-future-stability-of-source-code-archives-and-hashes 122 | [Google's `distroless` container images]: https://github.com/GoogleContainerTools/distroless 123 | [Arize AI]: https://www.arize.com 124 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If it is not security critical, please open an [issue](https://github.com/GoogleContainerTools/rules_distroless/issues) 6 | 7 | If it could be potentially exploited, or you are unsure if it can, 8 | please report privately via github [(instructions)](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability) 9 | and we will evaluate, fix and publish an advisory as necessary. 10 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/rules_distroless/cd8abc5d1c25d4cb87e3f48390514a946aa715eb/WORKSPACE -------------------------------------------------------------------------------- /apt/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_skylib//:bzl_library.bzl", "bzl_library") 2 | 3 | exports_files([ 4 | "apt.bzl", 5 | "extensions.bzl", 6 | ]) 7 | 8 | bzl_library( 9 | name = "defs", 10 | srcs = ["defs.bzl"], 11 | visibility = ["//visibility:public"], 12 | deps = [ 13 | "//apt/private:dpkg_status", 14 | "//apt/private:dpkg_statusd", 15 | ], 16 | ) 17 | 18 | bzl_library( 19 | name = "apt", 20 | srcs = ["apt.bzl"], 21 | visibility = ["//visibility:public"], 22 | deps = [ 23 | "//apt/private:deb_resolve", 24 | "//apt/private:deb_translate_lock", 25 | ], 26 | ) 27 | 28 | bzl_library( 29 | name = "extensions", 30 | srcs = ["extensions.bzl"], 31 | visibility = ["//visibility:public"], 32 | deps = [ 33 | "//apt/private:deb_import", 34 | "//apt/private:deb_resolve", 35 | "//apt/private:deb_translate_lock", 36 | "//apt/private:lockfile", 37 | "@bazel_features//:features", 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /apt/apt.bzl: -------------------------------------------------------------------------------- 1 | """ 2 | `apt.install` macro 3 | 4 | This documentation provides an overview of the convenience `apt.install` 5 | repository macro to create Debian repositories with packages "installed" in 6 | them and available to use in Bazel. 7 | """ 8 | 9 | load("//apt/private:deb_resolve.bzl", _deb_resolve = "deb_resolve") 10 | load("//apt/private:deb_translate_lock.bzl", _deb_translate_lock = "deb_translate_lock") 11 | 12 | def _apt_install( 13 | name, 14 | manifest, 15 | lock = None, 16 | nolock = False, 17 | package_template = None, 18 | resolve_transitive = True): 19 | """Repository macro to create Debian repositories. 20 | 21 | > [!WARNING] 22 | > THIS IS A LEGACY MACRO. Use it only if you are still using `WORKSPACE`. 23 | > Otherwise please use the [`apt` module extension](apt.md). 24 | 25 | Here's an example to create a Debian repo with `apt.install`: 26 | 27 | ```starlark 28 | # WORKSPACE 29 | 30 | load("@rules_distroless//apt:apt.bzl", "apt") 31 | 32 | apt.install( 33 | name = "bullseye", 34 | # lock = "//examples/apt:bullseye.lock.json", 35 | manifest = "//examples/apt:bullseye.yaml", 36 | ) 37 | 38 | load("@bullseye//:packages.bzl", "bullseye_packages") 39 | bullseye_packages() 40 | ``` 41 | 42 | Note that, for the initial setup (or if we want to run without a lock) the 43 | lockfile attribute can be omitted. All you need is a YAML 44 | [manifest](/examples/debian_snapshot/bullseye.yaml): 45 | ```yaml 46 | version: 1 47 | 48 | sources: 49 | - channel: bullseye main 50 | url: https://snapshot-cloudflare.debian.org/archive/debian/20240210T223313Z 51 | 52 | archs: 53 | - amd64 54 | 55 | packages: 56 | - perl 57 | ``` 58 | 59 | `apt.install` will parse the manifest and will fetch and install the 60 | packages for the given architectures in the Bazel repo `@`. 61 | 62 | Each `/` has two targets that match the usual structure of a 63 | Debian package: `data` and `control`. 64 | 65 | You can use the package like so: `@///:`. 66 | 67 | E.g. for the previous example, you could use `@bullseye//perl/amd64:data`. 68 | 69 | ### Lockfiles 70 | 71 | As mentioned, the macro can be used without a lock because the lock will be 72 | generated internally on-demand. However, this comes with the cost of 73 | performing a new package resolution on repository cache misses. 74 | 75 | The lockfile can be generated by running `bazel run @bullseye//:lock`. This 76 | will generate a `.lock.json` file of the same name and in the same path as 77 | the YAML `manifest` file. 78 | 79 | If you explicitly want to run without a lock and avoid the warning messages 80 | set the `nolock` argument to `True`. 81 | 82 | ### Best Practice: use snapshot archive URLs 83 | 84 | While we strongly encourage users to check in the generated lockfile, it's 85 | not always possible because Debian repositories are rolling by default. 86 | Therefore, a lockfile generated today might not work later if the upstream 87 | repository removes or publishes a new version of a package. 88 | 89 | To avoid this problems and increase the reproducibility it's recommended to 90 | avoid using normal Debian mirrors and use snapshot archives instead. 91 | 92 | Snapshot archives provide a way to access Debian package mirrors at a point 93 | in time. Basically, it's a "wayback machine" that allows access to (almost) 94 | all past and current packages based on dates and version numbers. 95 | 96 | Debian has had snapshot archives for [10+ 97 | years](https://lists.debian.org/debian-announce/2010/msg00002.html). Ubuntu 98 | began providing a similar service recently and has packages available since 99 | March 1st 2023. 100 | 101 | To use this services simply use a snapshot URL in the manifest. Here's two 102 | examples showing how to do this for Debian and Ubuntu: 103 | * [/examples/debian_snapshot](/examples/debian_snapshot) 104 | * [/examples/ubuntu_snapshot](/examples/ubuntu_snapshot) 105 | 106 | For more infomation, please check https://snapshot.debian.org and/or 107 | https://snapshot.ubuntu.com. 108 | 109 | Args: 110 | name: name of the repository 111 | manifest: label to a `manifest.yaml` 112 | lock: label to a `lock.json` 113 | nolock: bool, set to True if you explicitly want to run without a lock 114 | and avoid the DEBUG messages. 115 | package_template: (EXPERIMENTAL!) a template file for generated BUILD 116 | files. Available template replacement keys are: 117 | `{target_name}`, `{deps}`, `{urls}`, `{name}`, 118 | `{arch}`, `{sha256}`, `{repo_name}` 119 | resolve_transitive: whether dependencies of dependencies should be 120 | resolved and added to the lockfile. 121 | """ 122 | _deb_resolve( 123 | name = name + "_resolve", 124 | manifest = manifest, 125 | resolve_transitive = resolve_transitive, 126 | ) 127 | 128 | if not lock and not nolock: 129 | # buildifier: disable=print 130 | print("\nNo lockfile was given, please run `bazel run @%s//:lock` to create the lockfile." % name) 131 | 132 | _deb_translate_lock( 133 | name = name, 134 | lock = lock if lock else "@" + name + "_resolve//:lock.json", 135 | package_template = package_template, 136 | ) 137 | 138 | apt = struct( 139 | install = _apt_install, 140 | ) 141 | -------------------------------------------------------------------------------- /apt/defs.bzl: -------------------------------------------------------------------------------- 1 | "EXPERIMENTAL! Public API" 2 | 3 | load("//apt/private:dpkg_status.bzl", _dpkg_status = "dpkg_status") 4 | load("//apt/private:dpkg_statusd.bzl", _dpkg_statusd = "dpkg_statusd") 5 | 6 | dpkg_status = _dpkg_status 7 | dpkg_statusd = _dpkg_statusd 8 | -------------------------------------------------------------------------------- /apt/extensions.bzl: -------------------------------------------------------------------------------- 1 | "apt extensions" 2 | 3 | load("@bazel_features//:features.bzl", "bazel_features") 4 | load("//apt/private:deb_import.bzl", "deb_import") 5 | load("//apt/private:deb_resolve.bzl", "deb_resolve", "internal_resolve") 6 | load("//apt/private:deb_translate_lock.bzl", "deb_translate_lock") 7 | load("//apt/private:lockfile.bzl", "lockfile") 8 | 9 | def _distroless_extension(module_ctx): 10 | root_direct_deps = [] 11 | root_direct_dev_deps = [] 12 | reproducible = False 13 | 14 | for mod in module_ctx.modules: 15 | for install in mod.tags.install: 16 | lockf = None 17 | if not install.lock: 18 | lockf = internal_resolve( 19 | module_ctx, 20 | "yq", 21 | install.manifest, 22 | install.resolve_transitive, 23 | ) 24 | 25 | if not install.nolock: 26 | # buildifier: disable=print 27 | print("\nNo lockfile was given, please run `bazel run @%s//:lock` to create the lockfile." % install.name) 28 | else: 29 | lockf = lockfile.from_json(module_ctx, module_ctx.read(install.lock)) 30 | reproducible = True 31 | 32 | for (package) in lockf.packages(): 33 | package_key = lockfile.make_package_key( 34 | package["name"], 35 | package["version"], 36 | package["arch"], 37 | ) 38 | 39 | deb_import( 40 | name = "%s_%s" % (install.name, package_key), 41 | urls = package["urls"], 42 | sha256 = package["sha256"], 43 | mergedusr = install.mergedusr, 44 | ) 45 | 46 | deb_resolve( 47 | name = install.name + "_resolve", 48 | manifest = install.manifest, 49 | resolve_transitive = install.resolve_transitive, 50 | ) 51 | 52 | deb_translate_lock( 53 | name = install.name, 54 | lock = install.lock, 55 | lock_content = lockf.as_json(), 56 | package_template = install.package_template, 57 | ) 58 | 59 | if mod.is_root: 60 | if module_ctx.is_dev_dependency(install): 61 | root_direct_dev_deps.append(install.name) 62 | else: 63 | root_direct_deps.append(install.name) 64 | 65 | metadata_kwargs = {} 66 | if bazel_features.external_deps.extension_metadata_has_reproducible: 67 | metadata_kwargs["reproducible"] = reproducible 68 | 69 | return module_ctx.extension_metadata( 70 | root_module_direct_deps = root_direct_deps, 71 | root_module_direct_dev_deps = root_direct_dev_deps, 72 | **metadata_kwargs 73 | ) 74 | 75 | _install_doc = """ 76 | Module extension to create Debian repositories. 77 | 78 | Create Debian repositories with packages "installed" in them and available 79 | to use in Bazel. 80 | 81 | 82 | Here's an example how to create a Debian repo: 83 | 84 | ```starlark 85 | apt = use_extension("@rules_distroless//apt:extensions.bzl", "apt") 86 | apt.install( 87 | name = "bullseye", 88 | lock = "//examples/apt:bullseye.lock.json", 89 | manifest = "//examples/apt:bullseye.yaml", 90 | ) 91 | use_repo(apt, "bullseye") 92 | ``` 93 | 94 | Note that, for the initial setup (or if we want to run without a lock) the 95 | lockfile attribute can be omitted. All you need is a YAML 96 | [manifest](/examples/debian_snapshot/bullseye.yaml): 97 | ```yaml 98 | version: 1 99 | 100 | sources: 101 | - channel: bullseye main 102 | url: https://snapshot-cloudflare.debian.org/archive/debian/20240210T223313Z 103 | 104 | archs: 105 | - amd64 106 | 107 | packages: 108 | - perl 109 | ``` 110 | 111 | `apt.install` will parse the manifest and will fetch and install the packages 112 | for the given architectures in the Bazel repo `@`. 113 | 114 | Each `/` has two targets that match the usual structure of a 115 | Debian package: `data` and `control`. 116 | 117 | You can use the package like so: `@///:`. 118 | 119 | E.g. for the previous example, you could use `@bullseye//perl/amd64:data`. 120 | 121 | ### Lockfiles 122 | 123 | As mentioned, the macro can be used without a lock because the lock will be 124 | generated internally on-demand. However, this comes with the cost of 125 | performing a new package resolution on repository cache misses. 126 | 127 | The lockfile can be generated by running `bazel run @bullseye//:lock`. This 128 | will generate a `.lock.json` file of the same name and in the same path as 129 | the YAML `manifest` file. 130 | 131 | If you explicitly want to run without a lock and avoid the warning messages 132 | set the `nolock` argument to `True`. 133 | 134 | ### Best Practice: use snapshot archive URLs 135 | 136 | While we strongly encourage users to check in the generated lockfile, it's 137 | not always possible because Debian repositories are rolling by default. 138 | Therefore, a lockfile generated today might not work later if the upstream 139 | repository removes or publishes a new version of a package. 140 | 141 | To avoid this problems and increase the reproducibility it's recommended to 142 | avoid using normal Debian mirrors and use snapshot archives instead. 143 | 144 | Snapshot archives provide a way to access Debian package mirrors at a point 145 | in time. Basically, it's a "wayback machine" that allows access to (almost) 146 | all past and current packages based on dates and version numbers. 147 | 148 | Debian has had snapshot archives for [10+ 149 | years](https://lists.debian.org/debian-announce/2010/msg00002.html). Ubuntu 150 | began providing a similar service recently and has packages available since 151 | March 1st 2023. 152 | 153 | To use this services simply use a snapshot URL in the manifest. Here's two 154 | examples showing how to do this for Debian and Ubuntu: 155 | * [/examples/debian_snapshot](/examples/debian_snapshot) 156 | * [/examples/ubuntu_snapshot](/examples/ubuntu_snapshot) 157 | 158 | For more infomation, please check https://snapshot.debian.org and/or 159 | https://snapshot.ubuntu.com. 160 | """ 161 | 162 | install = tag_class( 163 | attrs = { 164 | "name": attr.string( 165 | doc = "Name of the generated repository", 166 | mandatory = True, 167 | ), 168 | "manifest": attr.label( 169 | doc = "The file used to generate the lock file", 170 | mandatory = True, 171 | ), 172 | "lock": attr.label( 173 | doc = "The lock file to use for the index.", 174 | ), 175 | "nolock": attr.bool( 176 | doc = "If you explicitly want to run without a lock, set it " + 177 | "to `True` to avoid the DEBUG messages.", 178 | default = False, 179 | ), 180 | "package_template": attr.label( 181 | doc = "(EXPERIMENTAL!) a template file for generated BUILD " + 182 | "files.", 183 | ), 184 | "resolve_transitive": attr.bool( 185 | doc = "Whether dependencies of dependencies should be " + 186 | "resolved and added to the lockfile.", 187 | default = True, 188 | ), 189 | "mergedusr": attr.bool( 190 | doc = "Whether packges should be normalized following mergedusr conventions.\n" + 191 | "Turning this on might fix the following error thrown by docker for ambigious paths: `duplicate of paths are supported.` \n" + 192 | "For more context please see https://salsa.debian.org/md/usrmerge/-/raw/master/debian/README.Debian?ref_type=heads", 193 | default = False, 194 | ), 195 | }, 196 | doc = _install_doc, 197 | ) 198 | 199 | apt = module_extension( 200 | implementation = _distroless_extension, 201 | tag_classes = { 202 | "install": install, 203 | }, 204 | ) 205 | -------------------------------------------------------------------------------- /apt/private/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_skylib//:bzl_library.bzl", "bzl_library") 2 | 3 | exports_files([ 4 | "dpkg_statusd.sh", 5 | "dpkg_status.sh", 6 | "copy.sh.tmpl", 7 | "package.BUILD.tmpl", 8 | ]) 9 | 10 | bzl_library( 11 | name = "dpkg_status", 12 | srcs = ["dpkg_status.bzl"], 13 | visibility = ["//apt:__subpackages__"], 14 | deps = ["//distroless/private:tar"], 15 | ) 16 | 17 | bzl_library( 18 | name = "dpkg_statusd", 19 | srcs = ["dpkg_statusd.bzl"], 20 | visibility = ["//apt:__subpackages__"], 21 | deps = ["//distroless/private:tar"], 22 | ) 23 | 24 | bzl_library( 25 | name = "deb_translate_lock", 26 | srcs = ["deb_translate_lock.bzl"], 27 | visibility = ["//apt:__subpackages__"], 28 | deps = [ 29 | ":lockfile", 30 | ":starlark_codegen_utils", 31 | "@bazel_skylib//lib:new_sets", 32 | "@bazel_tools//tools/build_defs/repo:cache.bzl", 33 | "@bazel_tools//tools/build_defs/repo:http.bzl", 34 | "@bazel_tools//tools/build_defs/repo:utils.bzl", 35 | ], 36 | ) 37 | 38 | bzl_library( 39 | name = "lockfile", 40 | srcs = ["lockfile.bzl"], 41 | visibility = ["//apt:__subpackages__"], 42 | deps = [":util"], 43 | ) 44 | 45 | bzl_library( 46 | name = "apt_deb_repository", 47 | srcs = ["apt_deb_repository.bzl"], 48 | visibility = ["//apt:__subpackages__"], 49 | deps = [ 50 | ":util", 51 | ":version_constraint", 52 | ], 53 | ) 54 | 55 | bzl_library( 56 | name = "apt_dep_resolver", 57 | srcs = ["apt_dep_resolver.bzl"], 58 | visibility = ["//apt:__subpackages__"], 59 | deps = [ 60 | ":version", 61 | ":version_constraint", 62 | ], 63 | ) 64 | 65 | bzl_library( 66 | name = "deb_resolve", 67 | srcs = ["deb_resolve.bzl"], 68 | visibility = ["//apt:__subpackages__"], 69 | deps = [ 70 | ":apt_deb_repository", 71 | ":apt_dep_resolver", 72 | ":lockfile", 73 | "@aspect_bazel_lib//lib:repo_utils", 74 | ], 75 | ) 76 | 77 | bzl_library( 78 | name = "version", 79 | srcs = ["version.bzl"], 80 | visibility = ["//apt:__subpackages__"], 81 | deps = ["@aspect_bazel_lib//lib:strings"], 82 | ) 83 | 84 | bzl_library( 85 | name = "deb_import", 86 | srcs = ["deb_import.bzl"], 87 | visibility = ["//apt:__subpackages__"], 88 | deps = ["@bazel_tools//tools/build_defs/repo:http.bzl"], 89 | ) 90 | 91 | bzl_library( 92 | name = "version_constraint", 93 | srcs = ["version_constraint.bzl"], 94 | visibility = ["//apt:__subpackages__"], 95 | deps = [":version"], 96 | ) 97 | 98 | bzl_library( 99 | name = "starlark_codegen_utils", 100 | srcs = ["starlark_codegen_utils.bzl"], 101 | visibility = ["//apt:__subpackages__"], 102 | ) 103 | 104 | bzl_library( 105 | name = "util", 106 | srcs = ["util.bzl"], 107 | visibility = ["//apt:__subpackages__"], 108 | ) 109 | -------------------------------------------------------------------------------- /apt/private/apt_deb_repository.bzl: -------------------------------------------------------------------------------- 1 | "https://wiki.debian.org/DebianRepository" 2 | 3 | load(":util.bzl", "util") 4 | load(":version_constraint.bzl", "version_constraint") 5 | 6 | def _fetch_package_index(rctx, urls, dist, comp, arch, integrity): 7 | target_triple = "{dist}/{comp}/{arch}".format(dist = dist, comp = comp, arch = arch) 8 | 9 | # See https://linux.die.net/man/1/xz , https://linux.die.net/man/1/gzip , and https://linux.die.net/man/1/bzip2 10 | # --keep -> keep the original file (Bazel might be still committing the output to the cache) 11 | # --force -> overwrite the output if it exists 12 | # --decompress -> decompress 13 | # Order of these matter, we want to try the one that is most likely first. 14 | supported_extensions = [ 15 | (".xz", ["xz", "--decompress", "--keep", "--force"]), 16 | (".gz", ["gzip", "--decompress", "--keep", "--force"]), 17 | (".bz2", ["bzip2", "--decompress", "--keep", "--force"]), 18 | ("", ["true"]), 19 | ] 20 | 21 | failed_attempts = [] 22 | 23 | url = None 24 | for url in urls: 25 | download = None 26 | for (ext, cmd) in supported_extensions: 27 | output = "{}/Packages{}".format(target_triple, ext) 28 | dist_url = "{}/dists/{}/{}/binary-{}/Packages{}".format(url, dist, comp, arch, ext) 29 | download = rctx.download( 30 | url = dist_url, 31 | output = output, 32 | integrity = integrity, 33 | allow_fail = True, 34 | ) 35 | decompress_r = None 36 | if download.success: 37 | decompress_r = rctx.execute(cmd + [output]) 38 | if decompress_r.return_code == 0: 39 | integrity = download.integrity 40 | break 41 | 42 | failed_attempts.append((dist_url, download, decompress_r)) 43 | 44 | if download.success: 45 | break 46 | 47 | if len(failed_attempts) == len(supported_extensions) * len(urls): 48 | attempt_messages = [] 49 | for (failed_url, download, decompress) in failed_attempts: 50 | reason = "unknown" 51 | if not download.success: 52 | reason = "Download failed. See warning above for details." 53 | elif decompress.return_code != 0: 54 | reason = "Decompression failed with non-zero exit code.\n\n{}\n{}".format(decompress.stderr, decompress.stdout) 55 | 56 | attempt_messages.append("""\n*) Failed '{}'\n\n{}""".format(failed_url, reason)) 57 | 58 | fail(""" 59 | ** Tried to download {} different package indices and all failed. 60 | 61 | {} 62 | """.format(len(failed_attempts), "\n".join(attempt_messages))) 63 | 64 | return ("{}/Packages".format(target_triple), url, integrity) 65 | 66 | def _parse_repository(state, contents, roots): 67 | last_key = "" 68 | pkg = {} 69 | for group in contents.split("\n\n"): 70 | for line in group.split("\n"): 71 | if line.strip() == "": 72 | continue 73 | if line[0] == " ": 74 | pkg[last_key] += "\n" + line 75 | continue 76 | 77 | # This allows for (more) graceful parsing of Package metadata (such as X-* attributes) 78 | # which may contain patterns that are non-standard. This logic is intended to closely follow 79 | # the Debian team's parser logic: 80 | # * https://salsa.debian.org/python-debian-team/python-debian/-/blob/master/src/debian/deb822.py?ref_type=heads#L788 81 | split = line.split(": ", 1) 82 | key = split[0] 83 | value = "" 84 | 85 | if len(split) == 2: 86 | value = split[1] 87 | 88 | if not last_key and len(pkg) == 0 and key != "Package": 89 | fail("Invalid debian package index format. Expected 'Package' as first key, got '{}'".format(key)) 90 | 91 | last_key = key 92 | pkg[key] = value 93 | 94 | if len(pkg.keys()) != 0: 95 | pkg["Roots"] = roots 96 | _add_package(state, pkg) 97 | last_key = "" 98 | pkg = {} 99 | 100 | def _add_package(state, package): 101 | util.set_dict( 102 | state.packages, 103 | value = package, 104 | keys = (package["Architecture"], package["Package"], package["Version"]), 105 | ) 106 | 107 | # https://www.debian.org/doc/debian-policy/ch-relationships.html#virtual-packages-provides 108 | if "Provides" in package: 109 | for virtual in version_constraint.parse_depends(package["Provides"]): 110 | providers = util.get_dict( 111 | state.virtual_packages, 112 | (package["Architecture"], virtual["name"]), 113 | [], 114 | ) 115 | 116 | # If multiple versions of a package expose the same virtual package, 117 | # we should only keep a single reference for the one with greater 118 | # version. 119 | for (i, (provider, provided_version)) in enumerate(providers): 120 | if package["Package"] == provider["Package"] and ( 121 | virtual["version"] == provided_version 122 | ): 123 | if version_constraint.relop( 124 | package["Version"], 125 | provider["Version"], 126 | ">>", 127 | ): 128 | providers[i] = (package, virtual["version"]) 129 | 130 | # Return since we found the same package + version. 131 | return 132 | 133 | # Otherwise, first time encountering package. 134 | providers.append((package, virtual["version"])) 135 | util.set_dict( 136 | state.virtual_packages, 137 | providers, 138 | (package["Architecture"], virtual["name"]), 139 | ) 140 | 141 | def _virtual_packages(state, name, arch): 142 | return util.get_dict(state.virtual_packages, [arch, name], []) 143 | 144 | def _package_versions(state, name, arch): 145 | return util.get_dict(state.packages, [arch, name], {}).keys() 146 | 147 | def _package(state, name, version, arch): 148 | return util.get_dict(state.packages, keys = (arch, name, version)) 149 | 150 | def _create(rctx, sources, archs): 151 | state = struct( 152 | packages = dict(), 153 | virtual_packages = dict(), 154 | ) 155 | 156 | for arch in archs: 157 | for (urls, dist, comp) in sources: 158 | # We assume that `url` does not contain a trailing forward slash when passing to 159 | # functions below. If one is present, remove it. Some HTTP servers do not handle 160 | # redirects properly when a path contains "//" 161 | # (ie. https://mymirror.com/ubuntu//dists/noble/stable/... may return a 404 162 | # on misconfigured HTTP servers) 163 | urls = [url.rstrip("/") for url in urls] 164 | 165 | rctx.report_progress("Fetching package index: {}/{} for {}".format(dist, comp, arch)) 166 | (output, _, _) = _fetch_package_index(rctx, urls, dist, comp, arch, "") 167 | 168 | # TODO: this is expensive to perform. 169 | rctx.report_progress("Parsing package index: {}/{} for {}".format(dist, comp, arch)) 170 | _parse_repository(state, rctx.read(output), urls) 171 | 172 | return struct( 173 | package_versions = lambda **kwargs: _package_versions(state, **kwargs), 174 | virtual_packages = lambda **kwargs: _virtual_packages(state, **kwargs), 175 | package = lambda **kwargs: _package(state, **kwargs), 176 | ) 177 | 178 | deb_repository = struct( 179 | new = _create, 180 | ) 181 | 182 | # TESTONLY: DO NOT DEPEND ON THIS 183 | def _create_test_only(): 184 | state = struct( 185 | packages = dict(), 186 | virtual_packages = dict(), 187 | ) 188 | 189 | def reset(): 190 | state.packages.clear() 191 | state.virtual_packages.clear() 192 | 193 | return struct( 194 | package_versions = lambda **kwargs: _package_versions(state, **kwargs), 195 | virtual_packages = lambda **kwargs: _virtual_packages(state, **kwargs), 196 | package = lambda **kwargs: _package(state, **kwargs), 197 | parse_repository = lambda contents: _parse_repository(state, contents, "http://nowhere"), 198 | packages = state.packages, 199 | reset = reset, 200 | ) 201 | 202 | DO_NOT_DEPEND_ON_THIS_TEST_ONLY = struct( 203 | new = _create_test_only, 204 | ) 205 | -------------------------------------------------------------------------------- /apt/private/apt_dep_resolver.bzl: -------------------------------------------------------------------------------- 1 | "package resolution" 2 | 3 | load(":version.bzl", version_lib = "version") 4 | load(":version_constraint.bzl", "version_constraint") 5 | 6 | def _resolve_package(state, name, version, arch): 7 | # First check if the constraint is satisfied by a virtual package 8 | virtual_packages = state.repository.virtual_packages(name = name, arch = arch) 9 | 10 | candidates = [ 11 | package 12 | for (package, provided_version) in virtual_packages 13 | # If no version constraint, all candidates are acceptable. 14 | # else, only candidates matching is_satisfied_by are acceptable. 15 | if not version or ( 16 | provided_version and version_constraint.is_satisfied_by(version, provided_version) 17 | ) 18 | ] 19 | 20 | if len(candidates) == 1: 21 | return candidates[0] 22 | 23 | if len(candidates) > 1: 24 | for package in candidates: 25 | # Return 'required' packages immediately since it is implicit that 26 | # they should exist on a default debian install. 27 | # https://wiki.debian.org/Proposals/EssentialOnDiet. 28 | # 29 | # Packages would ideally specify a default through an alternative: 30 | # 31 | # Depends: mawk | awk 32 | # 33 | # In the case of required packages, these defaults are not specified. 34 | if "Priority" in package and package["Priority"] == "required": 35 | return package 36 | 37 | # Otherwise, we can't disambiguate the virtual package providers so 38 | # choose none and warn. 39 | # buildifier: disable=print 40 | print("\nMultiple candidates for virtual package '{}': {}".format( 41 | name, 42 | [package["Package"] for package in candidates], 43 | )) 44 | 45 | # Get available versions of the package 46 | versions_by_arch = state.repository.package_versions(name = name, arch = arch) 47 | versions_by_any_arch = state.repository.package_versions(name = name, arch = "all") 48 | 49 | # Order packages by highest to lowest 50 | versions = version_lib.sort(versions_by_arch + versions_by_any_arch, reverse = True) 51 | 52 | selected_version = None 53 | 54 | if version: 55 | for av in versions: 56 | if version_constraint.relop(av, version[1], version[0]): 57 | selected_version = av 58 | 59 | # Since versions are ordered by hight to low, the first satisfied version will be 60 | # the highest version and rules_distroless ignores Priority field so it's safe. 61 | # TODO: rethink this `break` with https://github.com/GoogleContainerTools/rules_distroless/issues/34 62 | break 63 | elif len(versions) > 0: 64 | # First element in the versions list is the latest version. 65 | selected_version = versions[0] 66 | 67 | package = state.repository.package(name = name, version = selected_version, arch = arch) 68 | if not package: 69 | package = state.repository.package(name = name, version = selected_version, arch = "all") 70 | 71 | return package 72 | 73 | _ITERATION_MAX_ = 2147483646 74 | 75 | # For future: unfortunately this function uses a few state variables to track 76 | # certain conditions and package dependency groups. 77 | # TODO: Try to simplify it in the future. 78 | def _resolve_all(state, name, version, arch, include_transitive = True): 79 | root_package = None 80 | unmet_dependencies = [] 81 | dependencies = [] 82 | 83 | # state variables 84 | already_recursed = {} 85 | dependency_group = [] 86 | stack = [(name, version, -1)] 87 | 88 | for i in range(0, _ITERATION_MAX_ + 1): 89 | if not len(stack): 90 | break 91 | if i == _ITERATION_MAX_: 92 | fail("resolve_all exhausted") 93 | 94 | (name, version, dependency_group_idx) = stack.pop() 95 | 96 | # If this iteration is part of a dependency group, and the dependency group is already met, then skip this iteration. 97 | if dependency_group_idx > -1 and dependency_group[dependency_group_idx][0]: 98 | continue 99 | 100 | package = _resolve_package(state, name, version, arch) 101 | 102 | # If this package is not found and is part of a dependency group, then just skip it. 103 | if not package and dependency_group_idx > -1: 104 | continue 105 | 106 | # If this package is not found but is not part of a dependency group, then add it to unmet dependencies. 107 | if not package: 108 | unmet_dependencies.append((name, version)) 109 | continue 110 | 111 | # If this package was requested as part of a dependency group, then mark it's group as `dependency met` 112 | if dependency_group_idx > -1: 113 | dependency_group[dependency_group_idx] = (True, dependency_group[dependency_group_idx][1]) 114 | 115 | # set the root package, if this is the first iteration 116 | if i == 0: 117 | root_package = package 118 | 119 | key = package["Package"] 120 | 121 | # If we encountered package before in the transitive closure, skip it 122 | if key in already_recursed: 123 | continue 124 | 125 | # Do not add dependency if it's a root package to avoid circular dependency. 126 | if i != 0 and key != root_package["Package"]: 127 | # Add it to the dependencies 128 | already_recursed[key] = True 129 | dependencies.append(package) 130 | 131 | deps = [] 132 | 133 | # Extend the lookup with all the items in the dependency closure 134 | if "Pre-Depends" in package and include_transitive: 135 | deps.extend(version_constraint.parse_depends(package["Pre-Depends"])) 136 | 137 | # Extend the lookup with all the items in the dependency closure 138 | if "Depends" in package and include_transitive: 139 | deps.extend(version_constraint.parse_depends(package["Depends"])) 140 | 141 | for dep in deps: 142 | if type(dep) == "list": 143 | # create a dependency group 144 | new_dependency_group_idx = len(dependency_group) 145 | dependency_group.append((False, " | ".join([p["name"] for p in dep]))) 146 | 147 | # Dependencies should be searched left to right, given it is a 148 | # stack it means we need to push in reverse order. 149 | for gdep in reversed(dep): 150 | # TODO: arch 151 | stack.append((gdep["name"], gdep["version"], new_dependency_group_idx)) 152 | else: 153 | # TODO: arch 154 | stack.append((dep["name"], dep["version"], -1)) 155 | 156 | for (met, dep) in dependency_group: 157 | if not met: 158 | unmet_dependencies.append((dep, None)) 159 | 160 | return (root_package, dependencies, unmet_dependencies) 161 | 162 | def _create_resolution(repository): 163 | state = struct(repository = repository) 164 | return struct( 165 | resolve_all = lambda **kwargs: _resolve_all(state, **kwargs), 166 | resolve_package = lambda **kwargs: _resolve_package(state, **kwargs), 167 | ) 168 | 169 | dependency_resolver = struct( 170 | new = _create_resolution, 171 | ) 172 | -------------------------------------------------------------------------------- /apt/private/copy.sh.tmpl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o pipefail -o errexit -o nounset 4 | 5 | lock=$(realpath "$1") 6 | autofix=${{2:-}} 7 | 8 | cd "$BUILD_WORKING_DIRECTORY" 9 | 10 | echo 11 | echo "Writing lockfile to {workspace_relative_path}" 12 | cp "$lock" "{workspace_relative_path}" 13 | 14 | # Detect which file we wish the user to edit 15 | if [ -e "$BUILD_WORKSPACE_DIRECTORY/WORKSPACE" ]; then 16 | wksp_file="WORKSPACE" 17 | elif [ -e "$BUILD_WORKSPACE_DIRECTORY/WORKSPACE.bazel" ]; then 18 | wksp_file="WORKSPACE.bazel" 19 | elif [ -e "$BUILD_WORKSPACE_DIRECTORY/MODULE.bazel" ]; then 20 | wksp_file="MODULE.bazel" 21 | else 22 | echo>&2 "Error: no MODULE.bazel or WORKSPACE file was found" 23 | exit 1 24 | fi 25 | 26 | # Detect a vendored buildozer binary in canonical location (tools/buildozer) 27 | if [ -e "$BUILD_WORKSPACE_DIRECTORY/tools/buildozer" ]; then 28 | buildozer="tools/buildozer" 29 | else 30 | # Assume it's on the $PATH 31 | buildozer="buildozer" 32 | fi 33 | 34 | echo 35 | 36 | cmd="$buildozer 'set lock \"{lock_label}\"' $wksp_file:{repo_name}" 37 | 38 | if [[ "$autofix" == "--autofix" ]]; then 39 | eval "$cmd" 40 | else 41 | cat < "$$layer" 13 | ;; 14 | *data.tar.xz|*data.tar.zst|*data.tar.lzma) 15 | realpath "$$data_file" 16 | $(ZSTD_BIN) --force --decompress --stdout "$$data_file" | 17 | $(ZSTD_BIN) --compress --format=gzip - > "$$layer" 18 | ;; 19 | *) 20 | echo "ERROR: data file not supported: $$data_file" 21 | exit 1 22 | ;; 23 | esac 24 | """ 25 | toolchains = ["@zstd_toolchains//:resolved_toolchain"] 26 | 27 | # If mergedusr, then rewrite paths to hoist bins/libs from / of the fs to /usr counterpart. 28 | # Be careful with this option as it assumes that /usr/ is mounted as one filesystem. 29 | # Read more: 30 | # https://wiki.gentoo.org/wiki/Merge-usr 31 | # https://salsa.debian.org/md/usrmerge/raw/master/debian/README.Debian 32 | # https://www.freedesktop.org/wiki/Software/systemd/TheCaseForTheUsrMerge/ 33 | # Mapping taken from https://github.com/floppym/merge-usr/blob/15dd02207bdee7ca6720d7024e8c0ffdc166ed23/merge-usr#L17-L25 34 | # https://salsa.debian.org/md/usrmerge/-/tree/master/debian?ref_type=heads 35 | if mergedusr: 36 | toolchains = ["@bsd_tar_toolchains//:resolved_toolchain"] 37 | apply = """\ 38 | $(BSDTAR_BIN) --confirmation --gzip -cf "$$layer" \ 39 | -s "#^\\./bin/\\(.\\)#./usr/bin/\\1#" \ 40 | -s "#^\\./sbin/\\(.\\)#./usr/bin/\\1#" \ 41 | -s "#^\\./usr/sbin/\\(.\\)#./usr/bin/\\1#" \ 42 | -s "#^\\./lib/\\(.\\)#./usr/lib/\\1#" \ 43 | -s "#^\\./lib32/\\(.\\)#./usr/lib32/\\1#" \ 44 | -s "#^\\./lib64/\\(.\\)#./usr/lib64/\\1#" \ 45 | -s "#^\\./libx32/\\(.\\)#./usr/libx32/\\1#" \ 46 | "@$$data_file" 2< <( 47 | $(BSDTAR_BIN) -tvf "$$data_file" | awk '{ 48 | ORS="" 49 | keep="y" 50 | if (substr($$1, 1, 1) == "d" && (\\ 51 | $$9 == "./bin/" ||\\ 52 | $$9 == "./sbin/" ||\\ 53 | $$9 == "./usr/sbin/" ||\\ 54 | $$9 == "./lib/" ||\\ 55 | $$9 == "./lib32/" ||\\ 56 | $$9 == "./lib64/" ||\\ 57 | $$9 == "./libx32/" \\ 58 | )) { 59 | keep="n" 60 | } 61 | for (j=0; j<31; j++) print keep 62 | fflush() 63 | }' 64 | ) 65 | """ 66 | 67 | native.genrule( 68 | name = name, 69 | srcs = srcs, 70 | outs = outs, 71 | cmd = """ 72 | # Per the dpkg-dev man page: 73 | # https://manpages.debian.org/bookworm/dpkg-dev/deb.5.en.html 74 | # 75 | # Debian data.tar files can be: 76 | # - .tar uncompressed, supported since dpkg 1.10.24 77 | # - .tar compressed with 78 | # * gzip: .gz 79 | # * bzip2: .bz2, supported since dpkg 1.10.24 80 | # * lzma: .lzma, supported since dpkg 1.13.25 81 | # * xz: .xz, supported since dpkg 1.15.6 82 | # * zstd: .zst, supported since dpkg 1.21.18 83 | # 84 | # ZSTD_BIN can decompress all formats except bzip2 85 | # 86 | # The OCI image spec supports .tar and .tar compressed with gzip or zstd. 87 | # Bazel needs the output filename to be fixed in advanced so we settle for 88 | # gzip compression. 89 | 90 | data_file="$<" 91 | layer="$@" 92 | 93 | %s 94 | """ % apply, 95 | toolchains = toolchains, 96 | **kwargs 97 | ) 98 | -------------------------------------------------------------------------------- /apt/private/deb_resolve.bzl: -------------------------------------------------------------------------------- 1 | "repository rule for resolving and generating lockfile" 2 | 3 | load("@aspect_bazel_lib//lib:repo_utils.bzl", "repo_utils") 4 | load(":apt_deb_repository.bzl", "deb_repository") 5 | load(":apt_dep_resolver.bzl", "dependency_resolver") 6 | load(":lockfile.bzl", "lockfile") 7 | load(":util.bzl", "util") 8 | load(":version_constraint.bzl", "version_constraint") 9 | 10 | def _parse_manifest(rctx, yq_toolchain_prefix, manifest): 11 | is_windows = repo_utils.is_windows(rctx) 12 | host_yq = Label("@{}_{}//:yq{}".format(yq_toolchain_prefix, repo_utils.platform(rctx), ".exe" if is_windows else "")) 13 | 14 | if hasattr(rctx, "watch"): 15 | rctx.watch(manifest) 16 | 17 | yq_args = [ 18 | str(rctx.path(host_yq)), 19 | str(rctx.path(manifest)), 20 | "-o=json", 21 | ] 22 | result = rctx.execute(yq_args) 23 | if result.return_code: 24 | fail("failed to parse manifest yq. '{}' exited with {}: \nSTDOUT:\n{}\nSTDERR:\n{}".format(" ".join(yq_args), result.return_code, result.stdout, result.stderr)) 25 | 26 | return json.decode(result.stdout if result.stdout != "null" else "{}") 27 | 28 | # This function is shared between BZLMOD and WORKSPACE implementations. 29 | # INTERNAL: DO NOT DEPEND! 30 | # buildifier: disable=function-docstring-args 31 | def internal_resolve(rctx, yq_toolchain_prefix, manifest, include_transitive): 32 | manifest = _parse_manifest(rctx, yq_toolchain_prefix, manifest) 33 | 34 | if manifest["version"] != 1: 35 | fail("Unsupported manifest version, {}. Please use `version: 1` manifest.".format(manifest["version"])) 36 | 37 | if type(manifest["sources"]) != "list": 38 | fail("`sources` should be an array") 39 | 40 | if type(manifest["archs"]) != "list": 41 | fail("`archs` should be an array") 42 | 43 | if type(manifest["packages"]) != "list": 44 | fail("`packages` should be an array") 45 | 46 | sources = [] 47 | 48 | for src in manifest["sources"]: 49 | distr, components = src["channel"].split(" ", 1) 50 | for comp in components.split(" "): 51 | # TODO: only support urls before 1.0 52 | if "urls" in src: 53 | urls = src["urls"] 54 | elif "url" in src: 55 | urls = [src["url"]] 56 | else: 57 | fail("Source missing 'url' or 'urls' field") 58 | 59 | sources.append(( 60 | urls, 61 | distr, 62 | comp, 63 | )) 64 | 65 | repository = deb_repository.new(rctx, sources = sources, archs = manifest["archs"]) 66 | resolver = dependency_resolver.new(repository) 67 | lockf = lockfile.empty(rctx) 68 | 69 | resolved_count = 0 70 | 71 | for arch in manifest["archs"]: 72 | resolved_count = 0 73 | dep_constraint_set = {} 74 | for dep_constraint in manifest["packages"]: 75 | if dep_constraint in dep_constraint_set: 76 | fail("Duplicate package, {}. Please remove it from your manifest".format(dep_constraint)) 77 | dep_constraint_set[dep_constraint] = True 78 | 79 | constraint = version_constraint.parse_depends(dep_constraint).pop() 80 | 81 | rctx.report_progress("Resolving %s for %s" % (dep_constraint, arch)) 82 | (package, dependencies, unmet_dependencies) = resolver.resolve_all( 83 | name = constraint["name"], 84 | version = constraint["version"], 85 | arch = arch, 86 | include_transitive = include_transitive, 87 | ) 88 | 89 | if not package: 90 | fail("Unable to locate package `%s` for architecture: %s. It may only exist for specific set of architectures." % (dep_constraint, arch)) 91 | 92 | if len(unmet_dependencies): 93 | # buildifier: disable=print 94 | util.warning(rctx, "Following dependencies could not be resolved for %s: %s" % (constraint["name"], ",".join([up[0] for up in unmet_dependencies]))) 95 | 96 | lockf.add_package(package, arch) 97 | 98 | resolved_count += len(dependencies) + 1 99 | 100 | for dep in dependencies: 101 | lockf.add_package(dep, arch) 102 | lockf.add_package_dependency(package, dep, arch) 103 | 104 | rctx.report_progress("Resolved %d packages for %s" % (resolved_count, arch)) 105 | return lockf 106 | 107 | _BUILD_TMPL = """ 108 | load("@rules_shell//shell:sh_binary.bzl", "sh_binary") 109 | 110 | filegroup( 111 | name = "lockfile", 112 | srcs = ["lock.json"], 113 | tags = ["manual"], 114 | visibility = ["//visibility:public"] 115 | ) 116 | 117 | sh_binary( 118 | name = "lock", 119 | srcs = ["copy.sh"], 120 | data = ["lock.json"], 121 | tags = ["manual"], 122 | args = ["$(location :lock.json)"], 123 | visibility = ["//visibility:public"] 124 | ) 125 | """ 126 | 127 | def _deb_resolve_impl(rctx): 128 | lockf = internal_resolve(rctx, rctx.attr.yq_toolchain_prefix, rctx.attr.manifest, rctx.attr.resolve_transitive) 129 | lockf.write("lock.json") 130 | 131 | lock_filename = rctx.attr.manifest.name.replace(".yaml", ".lock.json") 132 | lock_label = rctx.attr.manifest.relative(lock_filename) 133 | workspace_relative_path = "{}{}".format( 134 | ("%s/" % lock_label.package) if lock_label.package else "", 135 | lock_label.name, 136 | ) 137 | 138 | rctx.file( 139 | "copy.sh", 140 | rctx.read(rctx.attr._copy_sh_tmpl).format( 141 | repo_name = util.get_repo_name(rctx.name).replace("_resolve", ""), 142 | lock_label = lock_label, 143 | workspace_relative_path = workspace_relative_path, 144 | ), 145 | executable = True, 146 | ) 147 | 148 | rctx.file("BUILD.bazel", _BUILD_TMPL) 149 | 150 | deb_resolve = repository_rule( 151 | implementation = _deb_resolve_impl, 152 | attrs = { 153 | "manifest": attr.label(), 154 | "resolve_transitive": attr.bool(default = True), 155 | "yq_toolchain_prefix": attr.string(default = "yq"), 156 | "_copy_sh_tmpl": attr.label( 157 | default = "//apt/private:copy.sh.tmpl", 158 | doc = "INTERNAL, DO NOT USE - " + 159 | "private attribute label to prevent repo restart", 160 | ), 161 | }, 162 | ) 163 | -------------------------------------------------------------------------------- /apt/private/deb_translate_lock.bzl: -------------------------------------------------------------------------------- 1 | "repository rule for generating a dependency graph from a lockfile." 2 | 3 | load(":lockfile.bzl", "lockfile") 4 | load(":starlark_codegen_utils.bzl", "starlark_codegen_utils") 5 | load(":util.bzl", "util") 6 | 7 | # header template for packages.bzl file 8 | _DEB_IMPORT_HEADER_TMPL = '''\ 9 | """Generated by rules_distroless. DO NOT EDIT.""" 10 | load("@rules_distroless//apt/private:deb_import.bzl", "deb_import") 11 | 12 | # buildifier: disable=function-docstring 13 | def {}_packages(): 14 | ''' 15 | 16 | # deb_import template for packages.bzl file 17 | _DEB_IMPORT_TMPL = '''\ 18 | deb_import( 19 | name = "{name}", 20 | urls = {urls}, 21 | sha256 = "{sha256}", 22 | ) 23 | ''' 24 | 25 | _PACKAGE_TEMPLATE = '''\ 26 | """Generated by rules_distroless. DO NOT EDIT.""" 27 | 28 | alias( 29 | name = "data", 30 | actual = select({data_targets}), 31 | visibility = ["//visibility:public"], 32 | ) 33 | 34 | alias( 35 | name = "control", 36 | actual = select({control_targets}), 37 | visibility = ["//visibility:public"], 38 | ) 39 | 40 | filegroup( 41 | name = "{target_name}", 42 | srcs = select({deps}) + [":data"], 43 | visibility = ["//visibility:public"], 44 | ) 45 | ''' 46 | 47 | _ROOT_BUILD_TMPL = """\ 48 | "Generated by rules_distroless. DO NOT EDIT." 49 | 50 | load("@rules_distroless//apt:defs.bzl", "dpkg_status") 51 | load("@rules_distroless//distroless:defs.bzl", "flatten") 52 | 53 | exports_files(['packages.bzl']) 54 | 55 | # Map Debian architectures to platform CPUs. 56 | # 57 | # For more info on Debian architectures, see: 58 | # * https://wiki.debian.org/SupportedArchitectures 59 | # * https://wiki.debian.org/ArchitectureSpecificsMemo 60 | # * https://www.debian.org/releases/stable/amd64/ch02s01.en.html#idm186 61 | # 62 | # For more info on Bazel's platforms CPUs see: 63 | # * https://github.com/bazelbuild/platforms/blob/main/cpu/BUILD 64 | _ARCHITECTURE_MAP = {{ 65 | "amd64": "x86_64", 66 | "arm64": "arm64", 67 | "ppc64el": "ppc64le", 68 | "mips64el": "mips64", 69 | "s390x": "s390x", 70 | "i386": "x86_32", 71 | "armhf": "armv7e-mf", 72 | "all": "all", 73 | }} 74 | 75 | _ARCHITECTURES = {architectures} 76 | 77 | [ 78 | config_setting( 79 | name = os + "_" + arch, 80 | constraint_values = [ 81 | "@platforms//os:" + os, 82 | "@platforms//cpu:" + _ARCHITECTURE_MAP[arch], 83 | ], 84 | ) 85 | for os in ["linux"] 86 | for arch in _ARCHITECTURES 87 | ] 88 | 89 | 90 | alias( 91 | name = "lock", 92 | actual = "@{target_name}_resolve//:lock", 93 | visibility = ["//visibility:public"], 94 | ) 95 | 96 | # List of installed packages. For now it's private. 97 | _PACKAGES = {packages} 98 | 99 | # Creates /var/lib/dpkg/status with installed package information. 100 | dpkg_status( 101 | name = "dpkg_status", 102 | controls = select({{ 103 | "//:linux_%s" % arch: ["//%s:control" % package for package in packages] 104 | for arch, packages in _PACKAGES.items() 105 | }}), 106 | visibility = ["//visibility:public"], 107 | ) 108 | 109 | filegroup( 110 | name = "packages", 111 | srcs = select({{ 112 | "//:linux_%s" % arch: ["//%s" % package for package in packages] 113 | for arch, packages in _PACKAGES.items() 114 | }}), 115 | visibility = ["//visibility:public"], 116 | ) 117 | 118 | 119 | # A filegroup that contains all the packages and the dpkg status file. 120 | filegroup( 121 | name = "{target_name}", 122 | srcs = [ 123 | ":dpkg_status", 124 | ":packages", 125 | ], 126 | visibility = ["//visibility:public"], 127 | ) 128 | 129 | flatten( 130 | name = "flat", 131 | tars = [ 132 | "{target_name}", 133 | ], 134 | deduplicate = True, 135 | visibility = ["//visibility:public"], 136 | ) 137 | """ 138 | 139 | def _deb_translate_lock_impl(rctx): 140 | lock_content = rctx.attr.lock_content 141 | package_template = rctx.read(rctx.attr.package_template) 142 | lockf = lockfile.from_json(rctx, lock_content if lock_content else rctx.read(rctx.attr.lock)) 143 | 144 | package_defs = [] 145 | 146 | if not lock_content: 147 | package_defs = [_DEB_IMPORT_HEADER_TMPL.format(rctx.attr.name)] 148 | 149 | if len(lockf.packages()) < 1: 150 | package_defs.append(" pass") 151 | 152 | # TODO: rework lockfile to include architecure information 153 | architectures = {} 154 | packages = {} 155 | 156 | for (package) in lockf.packages(): 157 | package_key = lockfile.make_package_key( 158 | package["name"], 159 | package["version"], 160 | package["arch"], 161 | ) 162 | 163 | if package["arch"] not in architectures: 164 | architectures[package["arch"]] = [] 165 | 166 | if package["name"] not in architectures[package["arch"]]: 167 | architectures[package["arch"]].append(package["name"]) 168 | 169 | if package["name"] not in packages: 170 | packages[package["name"]] = [] 171 | if package["arch"] not in packages[package["name"]]: 172 | packages[package["name"]].append(package["arch"]) 173 | 174 | if not lock_content: 175 | package_defs.append( 176 | _DEB_IMPORT_TMPL.format( 177 | name = "%s_%s" % (rctx.attr.name, package_key), 178 | package_name = package["name"], 179 | urls = package["urls"], 180 | sha256 = package["sha256"], 181 | ), 182 | ) 183 | 184 | repo_name = "%s%s_%s" % ("@" if lock_content else "", rctx.attr.name, package_key) 185 | 186 | rctx.file( 187 | "%s/%s/BUILD.bazel" % (package["name"], package["arch"]), 188 | package_template.format( 189 | target_name = package["arch"], 190 | data_targets = '"@%s//:data"' % repo_name, 191 | control_targets = '"@%s//:control"' % repo_name, 192 | src = '"@%s//:data"' % repo_name, 193 | deps = starlark_codegen_utils.to_list_attr([ 194 | "//%s/%s" % (dep["name"], package["arch"]) 195 | for dep in package["dependencies"] 196 | ]), 197 | urls = package["urls"], 198 | name = package["name"], 199 | arch = package["arch"], 200 | sha256 = package["sha256"], 201 | repo_name = "%s" % repo_name, 202 | ), 203 | ) 204 | 205 | # TODO: rework lockfile to include architecure information and merge these two loops 206 | for package_name, package_archs in packages.items(): 207 | rctx.file( 208 | "%s/BUILD.bazel" % (package_name), 209 | _PACKAGE_TEMPLATE.format( 210 | target_name = package_name, 211 | data_targets = starlark_codegen_utils.to_dict_attr({ 212 | "//:linux_%s" % arch: "//%s/%s:data" % (package_name, arch) 213 | for arch in package_archs 214 | }), 215 | control_targets = starlark_codegen_utils.to_dict_attr({ 216 | "//:linux_%s" % arch: "//%s/%s:control" % (package_name, arch) 217 | for arch in package_archs 218 | }), 219 | deps = starlark_codegen_utils.to_dict_list_attr({ 220 | "//:linux_%s" % arch: ["//%s/%s" % (package_name, arch)] 221 | for arch in package_archs 222 | }), 223 | ), 224 | ) 225 | 226 | rctx.file("packages.bzl", "\n".join(package_defs)) 227 | rctx.file("BUILD.bazel", _ROOT_BUILD_TMPL.format( 228 | target_name = util.get_repo_name(rctx.attr.name), 229 | packages = starlark_codegen_utils.to_dict_list_attr(architectures), 230 | architectures = starlark_codegen_utils.to_list_attr(architectures.keys()), 231 | )) 232 | 233 | deb_translate_lock = repository_rule( 234 | implementation = _deb_translate_lock_impl, 235 | attrs = { 236 | "lock": attr.label(), 237 | "lock_content": attr.string(doc = "INTERNAL: DO NOT USE"), 238 | "package_template": attr.label(default = "//apt/private:package.BUILD.tmpl"), 239 | }, 240 | ) 241 | -------------------------------------------------------------------------------- /apt/private/dpkg_status.bzl: -------------------------------------------------------------------------------- 1 | "dpkg_status" 2 | 3 | # buildifier: disable=bzl-visibility 4 | load("//distroless/private:tar.bzl", "tar_lib") 5 | 6 | _DOC = """TODO: docs""" 7 | 8 | def _dpkg_status_impl(ctx): 9 | bsdtar = ctx.toolchains[tar_lib.TOOLCHAIN_TYPE] 10 | 11 | output = ctx.actions.declare_file(ctx.attr.name + ".tar") 12 | 13 | args = ctx.actions.args() 14 | args.add(bsdtar.tarinfo.binary) 15 | args.add(output) 16 | args.add_all(ctx.files.controls) 17 | 18 | ctx.actions.run( 19 | executable = ctx.executable._dpkg_status_sh, 20 | inputs = ctx.files.controls, 21 | outputs = [output], 22 | tools = bsdtar.default.files, 23 | arguments = [args], 24 | ) 25 | 26 | return [ 27 | DefaultInfo(files = depset([output])), 28 | ] 29 | 30 | dpkg_status = rule( 31 | doc = _DOC, 32 | attrs = { 33 | "_dpkg_status_sh": attr.label( 34 | allow_single_file = True, 35 | executable = True, 36 | cfg = "exec", 37 | default = ":dpkg_status.sh", 38 | ), 39 | "controls": attr.label_list( 40 | allow_files = [".tar.zst", ".tar.xz", ".tar.gz", ".tar"], 41 | mandatory = True, 42 | ), 43 | }, 44 | implementation = _dpkg_status_impl, 45 | toolchains = [tar_lib.TOOLCHAIN_TYPE], 46 | ) 47 | -------------------------------------------------------------------------------- /apt/private/dpkg_status.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o pipefail -o errexit -o nounset 3 | 4 | readonly bsdtar="$1" 5 | readonly out="$2" 6 | shift 2 7 | 8 | tmp_out=$(mktemp) 9 | 10 | while (( $# > 0 )); do 11 | $bsdtar -xf "$1" --to-stdout ./control | 12 | awk '{ 13 | print $0; 14 | if (NR == 1) { print "Status: install ok installed"}; 15 | } END { print "" } 16 | ' >> $tmp_out 17 | shift 18 | done 19 | 20 | echo "#mtree 21 | ./var/lib/dpkg/status type=file uid=0 gid=0 mode=0644 time=1672560000 contents=$tmp_out 22 | " | "$bsdtar" $@ -cf "$out" "@-" 23 | 24 | rm $tmp_out 25 | -------------------------------------------------------------------------------- /apt/private/dpkg_statusd.bzl: -------------------------------------------------------------------------------- 1 | "dpkg_statusd" 2 | 3 | # buildifier: disable=bzl-visibility 4 | load("//distroless/private:tar.bzl", "tar_lib") 5 | 6 | _DOC = """TODO: docs""" 7 | 8 | def _dpkg_statusd_impl(ctx): 9 | bsdtar = ctx.toolchains[tar_lib.TOOLCHAIN_TYPE] 10 | 11 | ext = tar_lib.common.compression_to_extension[ctx.attr.compression] if ctx.attr.compression else ".tar" 12 | output = ctx.actions.declare_file(ctx.attr.name + ext) 13 | 14 | args = ctx.actions.args() 15 | args.add(bsdtar.tarinfo.binary) 16 | args.add(output) 17 | args.add(ctx.file.control) 18 | args.add(ctx.attr.package_name) 19 | tar_lib.common.add_compression_args(ctx.attr.compression, args) 20 | 21 | ctx.actions.run( 22 | executable = ctx.executable._dpkg_statusd_sh, 23 | inputs = [ctx.file.control], 24 | outputs = [output], 25 | tools = bsdtar.default.files, 26 | arguments = [args], 27 | ) 28 | 29 | return [ 30 | DefaultInfo(files = depset([output])), 31 | ] 32 | 33 | dpkg_statusd = rule( 34 | doc = _DOC, 35 | attrs = { 36 | "_dpkg_statusd_sh": attr.label( 37 | allow_single_file = True, 38 | executable = True, 39 | cfg = "exec", 40 | default = ":dpkg_statusd.sh", 41 | ), 42 | "package_name": attr.string(mandatory = True), 43 | "control": attr.label( 44 | allow_single_file = [".tar.zst", ".tar.xz", ".tar.gz", ".tar"], 45 | mandatory = True, 46 | ), 47 | "compression": attr.string( 48 | doc = "Compress the archive file with a supported algorithm.", 49 | values = tar_lib.common.accepted_compression_types, 50 | ), 51 | }, 52 | implementation = _dpkg_statusd_impl, 53 | toolchains = [tar_lib.TOOLCHAIN_TYPE], 54 | ) 55 | -------------------------------------------------------------------------------- /apt/private/dpkg_statusd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o pipefail -o errexit -o nounset 3 | 4 | readonly bsdtar="$1" 5 | readonly out="$2" 6 | readonly control_path="$3" 7 | readonly package_name="$4" 8 | shift 4 9 | 10 | include=(--include "^./control$" --include "^./md5sums$") 11 | 12 | tmp=$(mktemp -d) 13 | "$bsdtar" -xf "$control_path" "${include[@]}" -C "$tmp" 14 | 15 | "$bsdtar" -cf - $@ --format=mtree "${include[@]}" --options '!gname,!uname,!sha1,!nlink,!time' "@$control_path" | \ 16 | awk -v pkg="$package_name" '{ 17 | if ($1=="#mtree") { 18 | print $1; next 19 | }; 20 | # strip leading ./ prefix 21 | sub(/^\.?\//, "", $1); 22 | 23 | if ($1 ~ /^control/) { 24 | $1 = "./var/lib/dpkg/status.d/" pkg " contents=./" $1; 25 | } else if ($1 ~ /^md5sums/) { 26 | $1 = "./var/lib/dpkg/status.d/" pkg ".md5sums contents=./" $1; 27 | } 28 | print $0 29 | }' | "$bsdtar" $@ -cf "$out" -C "$tmp/" @- 30 | -------------------------------------------------------------------------------- /apt/private/lockfile.bzl: -------------------------------------------------------------------------------- 1 | "lock" 2 | 3 | load(":util.bzl", "util") 4 | 5 | def _make_package_key(name, version, arch): 6 | return "%s_%s_%s" % ( 7 | util.sanitize(name), 8 | util.sanitize(version), 9 | arch, 10 | ) 11 | 12 | def _package_key(package, arch): 13 | return _make_package_key(package["Package"], package["Version"], arch) 14 | 15 | def _add_package(lock, package, arch): 16 | k = _package_key(package, arch) 17 | if k in lock.fast_package_lookup: 18 | return 19 | lock.packages.append({ 20 | "key": k, 21 | "name": package["Package"], 22 | "version": package["Version"], 23 | "urls": [ 24 | "%s/%s" % (root, package["Filename"]) 25 | for root in package["Roots"] 26 | ], 27 | "sha256": package["SHA256"], 28 | "arch": arch, 29 | "dependencies": [], 30 | }) 31 | lock.fast_package_lookup[k] = len(lock.packages) - 1 32 | 33 | def _add_package_dependency(lock, package, dependency, arch): 34 | k = _package_key(package, arch) 35 | if k not in lock.fast_package_lookup: 36 | fail("Broken state: %s is not in the lockfile." % package["Package"]) 37 | i = lock.fast_package_lookup[k] 38 | lock.packages[i]["dependencies"].append(dict( 39 | key = _package_key(dependency, arch), 40 | name = dependency["Package"], 41 | version = dependency["Version"], 42 | )) 43 | 44 | def _has_package(lock, name, version, arch): 45 | key = "%s_%s_%s" % (util.sanitize(name), util.sanitize(version), arch) 46 | return key in lock.fast_package_lookup 47 | 48 | def _create(rctx, lock): 49 | return struct( 50 | has_package = lambda *args, **kwargs: _has_package(lock, *args, **kwargs), 51 | add_package = lambda *args, **kwargs: _add_package(lock, *args, **kwargs), 52 | add_package_dependency = lambda *args, **kwargs: _add_package_dependency(lock, *args, **kwargs), 53 | packages = lambda: lock.packages, 54 | write = lambda out: rctx.file(out, json.encode_indent(struct(version = lock.version, packages = lock.packages))), 55 | as_json = lambda: json.encode_indent(struct(version = lock.version, packages = lock.packages)), 56 | ) 57 | 58 | def _empty(rctx): 59 | lock = struct( 60 | version = 1, 61 | packages = list(), 62 | fast_package_lookup = dict(), 63 | ) 64 | return _create(rctx, lock) 65 | 66 | def _from_json(rctx, content): 67 | lock = json.decode(content) 68 | if lock["version"] != 1: 69 | fail("invalid lockfile version") 70 | 71 | lock = struct( 72 | version = lock["version"], 73 | packages = lock["packages"], 74 | fast_package_lookup = dict(), 75 | ) 76 | for (i, package) in enumerate(lock.packages): 77 | # TODO: only support urls before 1.0 78 | if "url" in package: 79 | package["urls"] = [package.pop("url")] 80 | 81 | lock.packages[i] = package 82 | lock.fast_package_lookup[package["key"]] = i 83 | return _create(rctx, lock) 84 | 85 | lockfile = struct( 86 | empty = _empty, 87 | from_json = _from_json, 88 | make_package_key = _make_package_key, 89 | ) 90 | -------------------------------------------------------------------------------- /apt/private/package.BUILD.tmpl: -------------------------------------------------------------------------------- 1 | """Generated by rules_distroless. DO NOT EDIT.""" 2 | 3 | alias( 4 | name = "data", 5 | actual = {data_targets}, 6 | visibility = ["//visibility:public"], 7 | ) 8 | 9 | alias( 10 | name = "control", 11 | actual = {control_targets}, 12 | visibility = ["//visibility:public"], 13 | ) 14 | 15 | filegroup( 16 | name = "{target_name}", 17 | srcs = {deps} + [":data"], 18 | visibility = ["//visibility:public"], 19 | ) -------------------------------------------------------------------------------- /apt/private/starlark_codegen_utils.bzl: -------------------------------------------------------------------------------- 1 | "Utilities for generating starlark source code" 2 | 3 | # https://github.com/aspect-build/rules_js/blob/main/npm/private/starlark_codegen_utils.bzl 4 | def _to_list_attr(list, indent_count = 0, indent_size = 4, quote_value = True): 5 | if not list: 6 | return "[]" 7 | tab = " " * indent_size 8 | indent = tab * indent_count 9 | result = "[" 10 | for v in list: 11 | val = "\"{}\"".format(v) if quote_value else v 12 | result += "\n%s%s%s," % (indent, tab, val) 13 | result += "\n%s]" % indent 14 | return result 15 | 16 | def _to_dict_attr(dict, indent_count = 0, indent_size = 4, quote_key = True, quote_value = True): 17 | if not len(dict): 18 | return "{}" 19 | tab = " " * indent_size 20 | indent = tab * indent_count 21 | result = "{" 22 | for k, v in dict.items(): 23 | key = "\"{}\"".format(k) if quote_key else k 24 | val = "\"{}\"".format(v) if quote_value else v 25 | result += "\n%s%s%s: %s," % (indent, tab, key, val) 26 | result += "\n%s}" % indent 27 | return result 28 | 29 | def _to_dict_list_attr(dict, indent_count = 0, indent_size = 4, quote_key = True): 30 | if not len(dict): 31 | return "{}" 32 | tab = " " * indent_size 33 | indent = tab * indent_count 34 | result = "{" 35 | for k, v in dict.items(): 36 | key = "\"{}\"".format(k) if quote_key else k 37 | v = _to_list_attr(v, indent_count + 1, indent_size) 38 | result += "\n%s%s%s: %s," % (indent, tab, key, v) 39 | result += "\n%s}" % indent 40 | return result 41 | 42 | starlark_codegen_utils = struct( 43 | to_list_attr = _to_list_attr, 44 | to_dict_attr = _to_dict_attr, 45 | to_dict_list_attr = _to_dict_list_attr, 46 | ) 47 | -------------------------------------------------------------------------------- /apt/private/util.bzl: -------------------------------------------------------------------------------- 1 | "utilities" 2 | 3 | def _set_dict(struct, value = None, keys = []): 4 | klen = len(keys) 5 | for i in range(klen - 1): 6 | k = keys[i] 7 | if not k in struct: 8 | struct[k] = {} 9 | struct = struct[k] 10 | 11 | struct[keys[-1]] = value 12 | 13 | def _get_dict(struct, keys = [], default_value = None): 14 | value = struct 15 | for k in keys: 16 | if k in value: 17 | value = value[k] 18 | else: 19 | value = default_value 20 | break 21 | return value 22 | 23 | def _sanitize(str): 24 | return str.replace("+", "-p-").replace(":", "-").replace("~", "_") 25 | 26 | def _get_repo_name(st): 27 | if st.find("+") != -1: 28 | return st.split("+")[-1] 29 | return st.split("~")[-1] 30 | 31 | def _warning(rctx, message): 32 | rctx.execute([ 33 | "echo", 34 | "\033[0;33mWARNING:\033[0m {}".format(message), 35 | ], quiet = False) 36 | 37 | util = struct( 38 | sanitize = _sanitize, 39 | set_dict = _set_dict, 40 | get_dict = _get_dict, 41 | warning = _warning, 42 | get_repo_name = _get_repo_name, 43 | ) 44 | -------------------------------------------------------------------------------- /apt/private/version.bzl: -------------------------------------------------------------------------------- 1 | "parse debian version strings" 2 | 3 | load("@aspect_bazel_lib//lib:strings.bzl", "ord") 4 | 5 | # https://www.debian.org/doc/debian-policy/ch-controlfields.html#version 6 | # https://github.com/Debian/apt/blob/main/apt-pkg/deb/debversion.cc 7 | def _parse_version(rv): 8 | epoch_idx = rv.find(":") 9 | epoch = None 10 | if epoch_idx != -1: 11 | epoch = rv[:epoch_idx] 12 | rv = rv[epoch_idx + 1:] 13 | 14 | revision_idx = rv.rfind("-") 15 | revision = None 16 | if revision_idx != -1: 17 | revision = rv[revision_idx + 1:] 18 | rv = rv[:revision_idx] 19 | 20 | upstream = rv 21 | 22 | return (epoch, upstream, revision) 23 | 24 | def _cmp(a, b): 25 | if a < b: 26 | return -1 27 | elif a > b: 28 | return 1 29 | return 0 30 | 31 | def _getdigits(st): 32 | return "".join([c for c in st.elems() if c.isdigit()]) 33 | 34 | def _order(char): 35 | if len(char) > 1: 36 | fail("expected a single char") 37 | if char == "~": 38 | return -1 39 | elif char.isdigit(): 40 | return int(char) + 1 41 | elif char.isalpha(): 42 | return ord(char) 43 | else: 44 | return ord(char) + 256 45 | 46 | def _version_cmp_string(va, vb): 47 | la = [_order(x) for x in va.elems()] 48 | lb = [_order(x) for x in vb.elems()] 49 | 50 | for i in range(max(len(la), len(lb))): 51 | a = 0 52 | b = 0 53 | if i < len(la): 54 | a = la[i] 55 | if i < len(lb): 56 | b = lb[i] 57 | res = _cmp(a, b) 58 | if res != 0: 59 | return res 60 | return 0 61 | 62 | # Iterate over the whole string and split it into groups of 63 | # numeric and non numeric portions. 64 | # a67bhgs89 -> 'a', '67', 'bhgs', '89'. 65 | # 2.7.2-linux-1 -> '2', '.', '7', '.' ,'-linux-','1' 66 | def _split_alpha_and_digit(v): 67 | v = v.elems() 68 | parts = [] 69 | current_part = "" 70 | for (i, c) in enumerate(v): 71 | # skip the first iteration as we just began grouping 72 | if i == 0: 73 | current_part = c 74 | continue 75 | p = v[i - 1] 76 | if c.isdigit() != p.isdigit(): 77 | parts.append(current_part) 78 | current_part = "" 79 | current_part += c 80 | 81 | # push leftover if theres any 82 | if current_part: 83 | parts.append(current_part) 84 | return parts 85 | 86 | # https://github.com/Debian/apt/blob/2845127968cda30be8423e1d3a24dae0e797bcc8/apt-pkg/deb/debversion.cc#L52 87 | def _version_cmp_part(va, vb): 88 | la = _split_alpha_and_digit(va) 89 | lb = _split_alpha_and_digit(vb) 90 | 91 | # compare alpha and digit parts of two strings 92 | for i in range(max(len(la), len(lb))): 93 | a = "0" 94 | b = "0" 95 | if i < len(la): 96 | a = la[i] 97 | if i < len(lb): 98 | b = lb[i] 99 | a_digits = _getdigits(a) 100 | b_digits = _getdigits(b) 101 | 102 | # compare if both parts are digits 103 | if a_digits and b_digits: 104 | a = int(a_digits) 105 | b = int(b_digits) 106 | res = _cmp(a, b) 107 | if res != 0: 108 | return res 109 | 110 | # do string comparison between the parts 111 | else: 112 | res = _version_cmp_string(a, b) 113 | if res != 0: 114 | return res 115 | return 0 116 | 117 | def _compare_version(va, vb): 118 | vap = _parse_version(va) 119 | vbp = _parse_version(vb) 120 | 121 | # compare epoch 122 | res = _cmp(int(vap[0] or "0"), int(vbp[0] or "0")) 123 | if res != 0: 124 | return res 125 | 126 | # compare upstream version 127 | res = _version_cmp_part(vap[1], vbp[1]) 128 | if res != 0: 129 | return res 130 | 131 | # compare debian revision 132 | return _version_cmp_part(vap[2] or "0", vbp[2] or "0") 133 | 134 | def _sort(versions, reverse = False): 135 | vr = versions 136 | for i in range(len(vr)): 137 | for j in range(i + 1, len(vr)): 138 | # if vr[i] is greater than vr[i+1] then swap their indices. 139 | if _compare_version(vr[i], vr[j]) == 1: 140 | vri = vr[i] 141 | vr[i] = vr[j] 142 | vr[j] = vri 143 | if reverse: 144 | vr = reversed(vr) 145 | return vr 146 | 147 | version = struct( 148 | parse = _parse_version, 149 | cmp = lambda va, vb: _compare_version(va, vb), 150 | gt = lambda va, vb: _compare_version(va, vb) == 1, 151 | gte = lambda va, vb: _compare_version(va, vb) >= 0, 152 | lt = lambda va, vb: _compare_version(va, vb) == -1, 153 | lte = lambda va, vb: _compare_version(va, vb) <= 0, 154 | eq = lambda va, vb: _compare_version(va, vb) == 0, 155 | sort = lambda versions, reverse = False: _sort(versions, reverse = reverse), 156 | ) 157 | -------------------------------------------------------------------------------- /apt/private/version_constraint.bzl: -------------------------------------------------------------------------------- 1 | "version constraint utilities" 2 | 3 | load(":version.bzl", version_lib = "version") 4 | 5 | def _parse_version_constraint(rawv): 6 | vconst_i = rawv.find(" ") 7 | if vconst_i == -1: 8 | fail('invalid version string %s expected a version constraint ">=", "=", ">=", "<<", ">>"' % rawv) 9 | return (rawv[:vconst_i], rawv[vconst_i + 1:]) 10 | 11 | def _parse_dep(raw): 12 | raw = raw.strip() # remove leading & trailing whitespace 13 | name = None 14 | version = None 15 | archs = None 16 | 17 | sqb_start_i = raw.find("[") 18 | if sqb_start_i != -1: 19 | sqb_end_i = raw.find("]") 20 | if sqb_end_i == -1: 21 | fail('invalid version string %s expected a closing brackets "]"' % raw) 22 | archs = raw[sqb_start_i + 1:sqb_end_i].strip().split(" ") 23 | raw = raw[:sqb_start_i] + raw[sqb_end_i + 1:] 24 | 25 | paren_start_i = raw.find("(") 26 | if paren_start_i != -1: 27 | paren_end_i = raw.find(")") 28 | if paren_end_i == -1: 29 | fail('invalid version string %s expected a closing paren ")"' % raw) 30 | name = raw[:paren_start_i].strip() 31 | version_and_const = raw[paren_start_i + 1:paren_end_i].strip() 32 | raw = raw[:paren_start_i] + raw[paren_end_i + 1:] 33 | version = _parse_version_constraint(version_and_const) 34 | 35 | # Depends: python3:any 36 | # is equivalent to 37 | # Depends: python3 [any] 38 | colon_i = raw.find(":") 39 | if colon_i != -1: 40 | arch_after_colon = raw[colon_i + 1:] 41 | raw = raw[:colon_i] 42 | archs = [arch_after_colon.strip()] 43 | 44 | name = raw.strip() 45 | return {"name": name, "version": version, "arch": archs} 46 | 47 | def _parse_depends(depends_raw): 48 | depends = [] 49 | for dep in depends_raw.split(","): 50 | if dep.find("|") != -1: 51 | depends.append([ 52 | _parse_dep(adep) 53 | for adep in dep.split("|") 54 | ]) 55 | else: 56 | depends.append(_parse_dep(dep)) 57 | 58 | return depends 59 | 60 | def _version_relop(va, vb, op): 61 | if op == "<<": 62 | return version_lib.lt(va, vb) 63 | elif op == ">>": 64 | return version_lib.gt(va, vb) 65 | elif op == "<=": 66 | return version_lib.lte(va, vb) 67 | elif op == ">=": 68 | return version_lib.gte(va, vb) 69 | elif op == "=": 70 | return version_lib.eq(va, vb) 71 | fail("unknown op %s" % op) 72 | 73 | def _is_satisfied_by(va, vb): 74 | if vb[0] != "=": 75 | fail("Per https://www.debian.org/doc/debian-policy/ch-relationships.html only = is allowed for Provides field.") 76 | 77 | return _version_relop(va[1], vb[1], va[0]) 78 | 79 | version_constraint = struct( 80 | relop = _version_relop, 81 | is_satisfied_by = _is_satisfied_by, 82 | parse_version_constraint = _parse_version_constraint, 83 | parse_depends = _parse_depends, 84 | parse_dep = _parse_dep, 85 | ) 86 | -------------------------------------------------------------------------------- /apt/tests/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load(":resolution_test.bzl", "resolution_tests") 2 | load(":version_test.bzl", "version_tests") 3 | 4 | version_tests() 5 | 6 | resolution_tests() 7 | -------------------------------------------------------------------------------- /apt/tests/resolution/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@aspect_bazel_lib//lib:jq.bzl", "jq") 2 | load("@aspect_bazel_lib//lib:testing.bzl", "assert_contains") 3 | load("@bazel_skylib//rules:build_test.bzl", "build_test") 4 | 5 | jq( 6 | name = "pick_libuuid_version", 7 | srcs = [ 8 | "@resolution_test_resolve//:lockfile", 9 | ], 10 | args = ["-rj"], 11 | filter = '.packages | map(select(.name == "libuuid1")) | .[0].version', 12 | ) 13 | 14 | assert_contains( 15 | name = "test_libuuid_version", 16 | actual = ":pick_libuuid_version", 17 | expected = "2.38.1-5+deb12u1", 18 | ) 19 | 20 | jq( 21 | name = "pick_quake_arch", 22 | srcs = [ 23 | "@arch_all_test_resolve//:lockfile", 24 | ], 25 | args = ["-rj"], 26 | filter = '.packages | map(select(.name == "quake")) | .[0].arch', 27 | ) 28 | 29 | assert_contains( 30 | name = "test_quake_arch", 31 | actual = ":pick_quake_arch", 32 | expected = "all", 33 | ) 34 | 35 | jq( 36 | name = "pick_quake_version", 37 | srcs = [ 38 | "@arch_all_test_resolve//:lockfile", 39 | ], 40 | args = ["-rj"], 41 | filter = '.packages | map(select(.name == "quake")) | .[0].version', 42 | ) 43 | 44 | assert_contains( 45 | name = "test_quake_version", 46 | actual = ":pick_quake_version", 47 | expected = "73", 48 | ) 49 | 50 | build_test( 51 | name = "build_clang", 52 | target_compatible_with = [ 53 | "@platforms//os:linux", 54 | ], 55 | targets = [ 56 | "@clang//clang", 57 | ], 58 | ) 59 | -------------------------------------------------------------------------------- /apt/tests/resolution/arch_all.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | sources: 4 | - channel: bookworm main contrib 5 | url: https://snapshot-cloudflare.debian.org/archive/debian/20240401T030239Z 6 | 7 | archs: 8 | - all 9 | 10 | packages: 11 | - quake 12 | -------------------------------------------------------------------------------- /apt/tests/resolution/clang.yaml: -------------------------------------------------------------------------------- 1 | # bazel run @jammy_cuda//:lock 2 | version: 1 3 | sources: 4 | - channel: jammy main 5 | url: https://snapshot.ubuntu.com/ubuntu/20240301T030400Z 6 | - channel: jammy universe 7 | url: https://snapshot.ubuntu.com/ubuntu/20240301T030400Z 8 | - channel: jammy-updates main 9 | url: https://snapshot.ubuntu.com/ubuntu/20240301T030400Z 10 | - channel: jammy-security main 11 | url: https://snapshot.ubuntu.com/ubuntu/20240301T030400Z 12 | archs: 13 | - "amd64" 14 | - "arm64" 15 | 16 | packages: 17 | - "clang" 18 | -------------------------------------------------------------------------------- /apt/tests/resolution/security.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | sources: 4 | - channel: bookworm main 5 | url: https://snapshot-cloudflare.debian.org/archive/debian/20240401T030239Z 6 | - channel: bookworm-updates main 7 | url: https://snapshot-cloudflare.debian.org/archive/debian/20240401T030239Z 8 | - channel: bookworm-security main 9 | url: https://snapshot-cloudflare.debian.org/archive/debian-security/20240401T030239Z 10 | 11 | archs: 12 | - amd64 13 | 14 | packages: 15 | - libuuid1 16 | -------------------------------------------------------------------------------- /apt/tests/resolution_test.bzl: -------------------------------------------------------------------------------- 1 | "unit tests for resolution of package dependencies" 2 | 3 | load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") 4 | load("//apt/private:apt_deb_repository.bzl", deb_repository = "DO_NOT_DEPEND_ON_THIS_TEST_ONLY") 5 | load("//apt/private:apt_dep_resolver.bzl", "dependency_resolver") 6 | load("//apt/private:version_constraint.bzl", "version_constraint") 7 | 8 | def _parse_depends_test(ctx): 9 | env = unittest.begin(ctx) 10 | 11 | parameters = { 12 | " | ".join([ 13 | "libc6 (>= 2.2.1), default-mta", 14 | "mail-transport-agent", 15 | ]): [ 16 | {"name": "libc6", "version": (">=", "2.2.1"), "arch": None}, 17 | [ 18 | {"name": "default-mta", "version": None, "arch": None}, 19 | {"name": "mail-transport-agent", "version": None, "arch": None}, 20 | ], 21 | ], 22 | ", ".join([ 23 | "libluajit5.1-dev [i386 amd64 powerpc mips]", 24 | "liblua5.1-dev [hurd-i386 ia64 s390x sparc]", 25 | ]): [ 26 | { 27 | "name": "libluajit5.1-dev", 28 | "version": None, 29 | "arch": ["i386", "amd64", "powerpc", "mips"], 30 | }, 31 | { 32 | "name": "liblua5.1-dev", 33 | "version": None, 34 | "arch": ["hurd-i386", "ia64", "s390x", "sparc"], 35 | }, 36 | ], 37 | " | ".join([ 38 | "emacs", 39 | "emacsen, make, debianutils (>= 1.7)", 40 | ]): [ 41 | [ 42 | {"name": "emacs", "version": None, "arch": None}, 43 | {"name": "emacsen", "version": None, "arch": None}, 44 | ], 45 | {"name": "make", "version": None, "arch": None}, 46 | {"name": "debianutils", "version": (">=", "1.7"), "arch": None}, 47 | ], 48 | ", ".join([ 49 | "libcap-dev [!kfreebsd-i386 !hurd-i386]", 50 | "autoconf", 51 | "debhelper (>> 5.0.0)", 52 | "file", 53 | "libc6 (>= 2.7-1)", 54 | "libpaper1", 55 | "psutils", 56 | ]): [ 57 | {"name": "libcap-dev", "version": None, "arch": ["!kfreebsd-i386", "!hurd-i386"]}, 58 | {"name": "autoconf", "version": None, "arch": None}, 59 | {"name": "debhelper", "version": (">>", "5.0.0"), "arch": None}, 60 | {"name": "file", "version": None, "arch": None}, 61 | {"name": "libc6", "version": (">=", "2.7-1"), "arch": None}, 62 | {"name": "libpaper1", "version": None, "arch": None}, 63 | {"name": "psutils", "version": None, "arch": None}, 64 | ], 65 | "python3:any": [ 66 | {"name": "python3", "version": None, "arch": ["any"]}, 67 | ], 68 | " | ".join([ 69 | "gcc-i686-linux-gnu (>= 4:10.2)", 70 | "gcc:i386, g++-i686-linux-gnu (>= 4:10.2)", 71 | "g++:i386, dpkg-cross", 72 | ]): [ 73 | [ 74 | {"name": "gcc-i686-linux-gnu", "version": (">=", "4:10.2"), "arch": None}, 75 | {"name": "gcc", "version": None, "arch": ["i386"]}, 76 | ], 77 | [ 78 | {"name": "g++-i686-linux-gnu", "version": (">=", "4:10.2"), "arch": None}, 79 | {"name": "g++", "version": None, "arch": ["i386"]}, 80 | ], 81 | {"name": "dpkg-cross", "version": None, "arch": None}, 82 | ], 83 | " | ".join([ 84 | "gcc-x86-64-linux-gnu (>= 4:10.2)", 85 | "gcc:amd64, g++-x86-64-linux-gnu (>= 4:10.2)", 86 | "g++:amd64, dpkg-cross", 87 | ]): [ 88 | [ 89 | {"name": "gcc-x86-64-linux-gnu", "version": (">=", "4:10.2"), "arch": None}, 90 | {"name": "gcc", "version": None, "arch": ["amd64"]}, 91 | ], 92 | [ 93 | {"name": "g++-x86-64-linux-gnu", "version": (">=", "4:10.2"), "arch": None}, 94 | {"name": "g++", "version": None, "arch": ["amd64"]}, 95 | ], 96 | {"name": "dpkg-cross", "version": None, "arch": None}, 97 | ], 98 | } 99 | 100 | for deps, expected in parameters.items(): 101 | actual = version_constraint.parse_depends(deps) 102 | asserts.equals(env, expected, actual) 103 | 104 | return unittest.end(env) 105 | 106 | parse_depends_test = unittest.make(_parse_depends_test) 107 | 108 | _test_version = "2.38.1-5" 109 | _test_arch = "amd64" 110 | 111 | def _make_index(): 112 | idx = deb_repository.new() 113 | resolution = dependency_resolver.new(idx) 114 | 115 | def _add_package(idx, **kwargs): 116 | kwargs["architecture"] = kwargs.get("architecture", _test_arch) 117 | kwargs["version"] = kwargs.get("version", _test_version) 118 | r = "\n".join(["{}: {}".format(item[0].title(), item[1]) for item in kwargs.items()]) 119 | idx.parse_repository(r) 120 | 121 | return struct( 122 | add_package = lambda **kwargs: _add_package(idx, **kwargs), 123 | resolution = resolution, 124 | reset = lambda: idx.reset(), 125 | ) 126 | 127 | def _resolve_optionals_test(ctx): 128 | env = unittest.begin(ctx) 129 | 130 | idx = _make_index() 131 | 132 | # Should pick the first alternative 133 | idx.add_package(package = "libc6-dev") 134 | idx.add_package(package = "eject", depends = "libc6-dev | libc-dev") 135 | 136 | (root_package, dependencies, _) = idx.resolution.resolve_all( 137 | name = "eject", 138 | version = ("=", _test_version), 139 | arch = _test_arch, 140 | ) 141 | asserts.equals(env, "eject", root_package["Package"]) 142 | asserts.equals(env, "libc6-dev", dependencies[0]["Package"]) 143 | asserts.equals(env, 1, len(dependencies)) 144 | 145 | return unittest.end(env) 146 | 147 | resolve_optionals_test = unittest.make(_resolve_optionals_test) 148 | 149 | def _resolve_architecture_specific_packages_test(ctx): 150 | env = unittest.begin(ctx) 151 | 152 | idx = _make_index() 153 | 154 | # Should pick bar for amd64 and foo for i386 155 | idx.add_package(package = "foo", architecture = "i386") 156 | idx.add_package(package = "bar", architecture = "amd64") 157 | idx.add_package(package = "glibc", architecture = "all", depends = "foo [i386], bar [amd64]") 158 | 159 | # bar for amd64 160 | (root_package, dependencies, _) = idx.resolution.resolve_all( 161 | name = "glibc", 162 | version = ("=", _test_version), 163 | arch = "amd64", 164 | ) 165 | asserts.equals(env, "glibc", root_package["Package"]) 166 | asserts.equals(env, "all", root_package["Architecture"]) 167 | asserts.equals(env, "bar", dependencies[0]["Package"]) 168 | asserts.equals(env, 1, len(dependencies)) 169 | 170 | # foo for i386 171 | (root_package, dependencies, _) = idx.resolution.resolve_all( 172 | name = "glibc", 173 | version = ("=", _test_version), 174 | arch = "i386", 175 | ) 176 | asserts.equals(env, "glibc", root_package["Package"]) 177 | asserts.equals(env, "all", root_package["Architecture"]) 178 | asserts.equals(env, "foo", dependencies[0]["Package"]) 179 | asserts.equals(env, 1, len(dependencies)) 180 | 181 | return unittest.end(env) 182 | 183 | resolve_architecture_specific_packages_test = unittest.make(_resolve_architecture_specific_packages_test) 184 | 185 | def _resolve_aliases(ctx): 186 | env = unittest.begin(ctx) 187 | 188 | def with_package(**kwargs): 189 | def add_package(idx): 190 | idx.add_package(**kwargs) 191 | 192 | return add_package 193 | 194 | def check_resolves(with_packages, resolved_name): 195 | idx = _make_index() 196 | 197 | for package in with_packages: 198 | package(idx) 199 | 200 | (root_package, dependencies, _) = idx.resolution.resolve_all( 201 | name = "foo", 202 | version = ("=", _test_version), 203 | arch = "amd64", 204 | ) 205 | asserts.equals(env, "foo", root_package["Package"]) 206 | asserts.equals(env, "amd64", root_package["Architecture"]) 207 | 208 | if resolved_name: 209 | asserts.equals(env, 1, len(dependencies)) 210 | asserts.equals(env, resolved_name, dependencies[0]["Package"]) 211 | else: 212 | asserts.equals(env, 0, len(dependencies)) 213 | 214 | # Version match 215 | check_resolves([ 216 | with_package(package = "foo", depends = "bar (>= 1.0)"), 217 | with_package(package = "bar", version = "0.9"), 218 | with_package(package = "bar-plus", provides = "bar (= 1.0)"), 219 | ], resolved_name = "bar-plus") 220 | 221 | # Version match, ignores un-versioned 222 | check_resolves([ 223 | with_package(package = "foo", depends = "bar (>= 1.0)"), 224 | with_package(package = "bar", version = "0.9"), 225 | with_package(package = "bar-plus", provides = "bar (= 1.0)"), 226 | with_package(package = "bar-clone", provides = "bar"), 227 | ], resolved_name = "bar-plus") 228 | 229 | # Un-versioned match 230 | check_resolves([ 231 | with_package(package = "foo", depends = "bar"), 232 | with_package(package = "bar-plus", provides = "bar"), 233 | ], resolved_name = "bar-plus") 234 | 235 | # Un-versioned match, multiple provides 236 | check_resolves([ 237 | with_package(package = "foo", depends = "bar"), 238 | with_package(package = "bar-plus", provides = "bar, baz"), 239 | ], resolved_name = "bar-plus") 240 | 241 | # Un-versioned match, versioned provides 242 | check_resolves([ 243 | with_package(package = "foo", depends = "bar"), 244 | with_package(package = "bar-plus", provides = "bar (= 1.0)"), 245 | ], resolved_name = "bar-plus") 246 | 247 | # Un-versioned does not match with multiple candidates 248 | check_resolves([ 249 | with_package(package = "foo", depends = "bar"), 250 | with_package(package = "bar-plus", provides = "bar"), 251 | with_package(package = "bar-plus2", provides = "bar"), 252 | ], resolved_name = None) 253 | 254 | return unittest.end(env) 255 | 256 | resolve_aliases_test = unittest.make(_resolve_aliases) 257 | 258 | def _resolve_circular_deps_test(ctx): 259 | env = unittest.begin(ctx) 260 | 261 | idx = _make_index() 262 | 263 | # `ruby` dependencies should have no `ruby` 264 | idx.add_package(package = "libruby") 265 | idx.add_package(package = "ruby3.1", depends = "ruby") 266 | idx.add_package(package = "ruby-rubygems", depends = "ruby3.1") 267 | idx.add_package(package = "ruby", depends = "libruby, ruby-rubygems") 268 | 269 | (root_package, dependencies, _) = idx.resolution.resolve_all( 270 | name = "ruby", 271 | version = "", 272 | arch = _test_arch, 273 | ) 274 | asserts.equals(env, "ruby", root_package["Package"]) 275 | asserts.equals(env, "ruby-rubygems", dependencies[0]["Package"]) 276 | asserts.equals(env, 3, len(dependencies)) 277 | asserts.false(env, "ruby" in [d["Package"] for d in dependencies], "Circular `ruby` dependency") 278 | 279 | return unittest.end(env) 280 | 281 | resolve_circular_deps_test = unittest.make(_resolve_circular_deps_test) 282 | 283 | _TEST_SUITE_PREFIX = "package_resolution/" 284 | 285 | def resolution_tests(): 286 | """Repository macro to create package resolution tests. 287 | 288 | The tests cover: 289 | - parse depend packages, e.g., 290 | ``` 291 | Package: gcc 292 | Depends: gcc-i686-linux-gnu, gcc-x86-64-linux-gnu 293 | ``` 294 | - resolve optional packages, e.g., 295 | ``` 296 | Package: libc6-dev-amd64 297 | Depends: libc6-dev | libc-dev 298 | ``` 299 | - resolve architecture specific packages, e.g., 300 | ``` 301 | Package: gcc:amd64 302 | ``` 303 | - resolve aliases, e.g., 304 | ``` 305 | Package: foo 306 | Depends: bar (>= 1.0) 307 | 308 | Package: bar-plus 309 | Provides: bar (= 1.0) 310 | ``` 311 | - resolve circular dependencies, e.g., 312 | ``` 313 | Package: ruby 314 | Depends: ruby-rubygems 315 | 316 | Package: ruby-rubygems 317 | Depends: ruby3.1 318 | 319 | Package: ruby3.1 320 | Depends: ruby 321 | ``` 322 | """ 323 | parse_depends_test(name = _TEST_SUITE_PREFIX + "parse_depends") 324 | resolve_optionals_test(name = _TEST_SUITE_PREFIX + "resolve_optionals") 325 | resolve_architecture_specific_packages_test(name = _TEST_SUITE_PREFIX + "resolve_architectures_specific") 326 | resolve_aliases_test(name = _TEST_SUITE_PREFIX + "resolve_aliases") 327 | resolve_circular_deps_test(name = _TEST_SUITE_PREFIX + "parse_circular") 328 | -------------------------------------------------------------------------------- /apt/tests/version_test.bzl: -------------------------------------------------------------------------------- 1 | "unit tests for version parsing" 2 | 3 | load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") 4 | load("//apt/private:version.bzl", "version") 5 | load("//apt/private:version_constraint.bzl", "version_constraint") 6 | 7 | _TEST_SUITE_PREFIX = "version/" 8 | 9 | def _parse_test(ctx): 10 | parameters = { 11 | "1:1.4.1-1": ("1", "1.4.1", "1"), 12 | "7.1.ds-1": (None, "7.1.ds", "1"), 13 | "10.11.1.3-2": (None, "10.11.1.3", "2"), 14 | "4.0.1.3.dfsg.1-2": (None, "4.0.1.3.dfsg.1", "2"), 15 | "0.4.23debian1": (None, "0.4.23debian1", None), 16 | "1.2.10+cvs20060429-1": (None, "1.2.10+cvs20060429", "1"), 17 | "0.2.0-1+b1": (None, "0.2.0", "1+b1"), 18 | "4.3.90.1svn-r21976-1": (None, "4.3.90.1svn-r21976", "1"), 19 | "1.5+E-14": (None, "1.5+E", "14"), 20 | "20060611-0.0": (None, "20060611", "0.0"), 21 | "0.52.2-5.1": (None, "0.52.2", "5.1"), 22 | "7.0-035+1": (None, "7.0", "035+1"), 23 | "1.1.0+cvs20060620-1+2.6.15-8": (None, "1.1.0+cvs20060620-1+2.6.15", "8"), 24 | "1.1.0+cvs20060620-1+1.0": (None, "1.1.0+cvs20060620", "1+1.0"), 25 | "4.2.0a+stable-2sarge1": (None, "4.2.0a+stable", "2sarge1"), 26 | "1.8RC4b": (None, "1.8RC4b", None), 27 | "0.9~rc1-1": (None, "0.9~rc1", "1"), 28 | "2:1.0.4+svn26-1ubuntu1": ("2", "1.0.4+svn26", "1ubuntu1"), 29 | "2:1.0.4~rc2-1": ("2", "1.0.4~rc2", "1"), 30 | } 31 | 32 | env = unittest.begin(ctx) 33 | 34 | for v, expected in parameters.items(): 35 | actual = version.parse(v) 36 | asserts.equals(env, actual, expected) 37 | 38 | return unittest.end(env) 39 | 40 | parse_test = unittest.make(_parse_test) 41 | 42 | def _operators_test(ctx): 43 | parameters = [ 44 | ("0", version.lt, "a"), 45 | ("1.0", version.lt, "1.1"), 46 | ("1.2", version.lt, "1.11"), 47 | ("1.0-0.1", version.lt, "1.1"), 48 | ("1.0-0.1", version.lt, "1.0-1"), 49 | ("1.0", version.eq, "1.0"), 50 | ("1.0-0.1", version.eq, "1.0-0.1"), 51 | ("1:1.0-0.1", version.eq, "1:1.0-0.1"), 52 | ("1:1.0", version.eq, "1:1.0"), 53 | ("1.0-0.1", version.lt, "1.0-1"), 54 | ("1.0final-5sarge1", version.gt, "1.0final-5"), 55 | ("1.0final-5", version.gt, "1.0a7-2"), 56 | ("0.9.2-5", version.lt, "0.9.2+cvs.1.0.dev.2004.07.28-1.5"), 57 | ("1:500", version.lt, "1:5000"), 58 | ("100:500", version.gt, "11:5000"), 59 | ("1.0.4-2", version.gt, "1.0pre7-2"), 60 | ("1.5~rc1", version.lt, "1.5"), 61 | ("1.5~rc1", version.lt, "1.5+b1"), 62 | ("1.5~rc1", version.lt, "1.5~rc2"), 63 | ("1.5~rc1", version.gt, "1.5~dev0"), 64 | ] 65 | 66 | env = unittest.begin(ctx) 67 | for va, version_op, vb in parameters: 68 | asserts.true(env, version_op(va, vb)) 69 | 70 | return unittest.end(env) 71 | 72 | operators_test = unittest.make(_operators_test) 73 | 74 | def _sort_test(ctx): 75 | parameters = [ 76 | ( 77 | ["1.5~rc2", "1.0.4-2", "1.5~rc1"], 78 | ["1.0.4-2", "1.5~rc1", "1.5~rc2"], 79 | False, 80 | ), 81 | ( 82 | ["1.0a7-2", "1.0final-5sarge1", "1.0final-5"], 83 | ["1.0final-5sarge1", "1.0final-5", "1.0a7-2"], 84 | True, 85 | ), 86 | ] 87 | 88 | env = unittest.begin(ctx) 89 | 90 | for to_sort, expected, reversed in parameters: 91 | actual = version.sort(to_sort, reverse = reversed) 92 | asserts.equals(env, expected, actual) 93 | 94 | return unittest.end(env) 95 | 96 | sort_test = unittest.make(_sort_test) 97 | 98 | def _is_satisfied_by_test(ctx): 99 | parameters = [ 100 | (">= 1.1", "= 1.1", True), 101 | ("<= 1.1", "= 1.1", True), 102 | (">> 1.1", "= 1.1", False), 103 | ] 104 | 105 | env = unittest.begin(ctx) 106 | for va, vb, expected in parameters: 107 | asserts.equals( 108 | env, 109 | expected, 110 | version_constraint.is_satisfied_by( 111 | version_constraint.parse_version_constraint(va), 112 | version_constraint.parse_version_constraint(vb), 113 | ), 114 | ) 115 | 116 | return unittest.end(env) 117 | 118 | is_satisfied_by_test = unittest.make(_is_satisfied_by_test) 119 | 120 | def version_tests(): 121 | operators_test(name = _TEST_SUITE_PREFIX + "operators") 122 | parse_test(name = _TEST_SUITE_PREFIX + "parse") 123 | sort_test(name = _TEST_SUITE_PREFIX + "sort") 124 | is_satisfied_by_test(name = _TEST_SUITE_PREFIX + "is_satisfied_by") 125 | -------------------------------------------------------------------------------- /distroless/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_skylib//:bzl_library.bzl", "bzl_library") 2 | 3 | # For stardoc to reference the files 4 | exports_files(["defs.bzl"]) 5 | 6 | bzl_library( 7 | name = "dependencies", 8 | srcs = ["dependencies.bzl"], 9 | visibility = ["//visibility:public"], 10 | deps = [ 11 | "@bazel_tools//tools/build_defs/repo:http.bzl", 12 | "@bazel_tools//tools/build_defs/repo:utils.bzl", 13 | ], 14 | ) 15 | 16 | bzl_library( 17 | name = "defs", 18 | srcs = ["defs.bzl"], 19 | visibility = ["//visibility:public"], 20 | deps = [ 21 | "//distroless/private:cacerts", 22 | "//distroless/private:flatten", 23 | "//distroless/private:group", 24 | "//distroless/private:home", 25 | "//distroless/private:java_keystore", 26 | "//distroless/private:locale", 27 | "//distroless/private:os_release", 28 | "//distroless/private:passwd", 29 | ], 30 | ) 31 | 32 | bzl_library( 33 | name = "toolchains", 34 | srcs = ["toolchains.bzl"], 35 | visibility = ["//visibility:public"], 36 | deps = ["@aspect_bazel_lib//lib:repositories"], 37 | ) 38 | -------------------------------------------------------------------------------- /distroless/defs.bzl: -------------------------------------------------------------------------------- 1 | "Public API re-exports" 2 | 3 | load("//distroless/private:cacerts.bzl", _cacerts = "cacerts") 4 | load("//distroless/private:flatten.bzl", _flatten = "flatten") 5 | load("//distroless/private:group.bzl", _group = "group") 6 | load("//distroless/private:home.bzl", _home = "home") 7 | load("//distroless/private:java_keystore.bzl", _java_keystore = "java_keystore") 8 | load("//distroless/private:locale.bzl", _locale = "locale") 9 | load("//distroless/private:os_release.bzl", _os_release = "os_release") 10 | load("//distroless/private:passwd.bzl", _passwd = "passwd") 11 | 12 | cacerts = _cacerts 13 | locale = _locale 14 | os_release = _os_release 15 | group = _group 16 | passwd = _passwd 17 | java_keystore = _java_keystore 18 | home = _home 19 | flatten = _flatten 20 | -------------------------------------------------------------------------------- /distroless/dependencies.bzl: -------------------------------------------------------------------------------- 1 | """Declare runtime dependencies 2 | 3 | These are needed for local dev, and users must install them as well. 4 | See https://docs.bazel.build/versions/main/skylark/deploying.html#dependencies 5 | """ 6 | 7 | load("@bazel_tools//tools/build_defs/repo:http.bzl", _http_archive = "http_archive") 8 | load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") 9 | 10 | def http_archive(name, **kwargs): 11 | maybe(_http_archive, name = name, **kwargs) 12 | 13 | # WARNING: any changes in this function may be BREAKING CHANGES for users 14 | # because we'll fetch a dependency which may be different from one that 15 | # they were previously fetching later in their WORKSPACE setup, and now 16 | # ours took precedence. Such breakages are challenging for users, so any 17 | # changes in this function should be marked as BREAKING in the commit message 18 | # and released only in semver majors. 19 | # This is all fixed by bzlmod, so we just tolerate it for now. 20 | def distroless_dependencies(): 21 | # The minimal version of bazel_skylib we require 22 | http_archive( 23 | name = "bazel_skylib", 24 | sha256 = "74d544d96f4a5bb630d465ca8bbcfe231e3594e5aae57e1edbf17a6eb3ca2506", 25 | urls = [ 26 | "https://github.com/bazelbuild/bazel-skylib/releases/download/1.3.0/bazel-skylib-1.3.0.tar.gz", 27 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.3.0/bazel-skylib-1.3.0.tar.gz", 28 | ], 29 | ) 30 | 31 | http_archive( 32 | name = "aspect_bazel_lib", 33 | sha256 = "87ab4ec479ebeb00d286266aca2068caeef1bb0b1765e8f71c7b6cfee6af4226", 34 | strip_prefix = "bazel-lib-2.7.3", 35 | url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.7.3/bazel-lib-v2.7.3.tar.gz", 36 | ) 37 | 38 | http_archive( 39 | name = "rules_java", 40 | urls = [ 41 | "https://github.com/bazelbuild/rules_java/releases/download/8.3.2/rules_java-8.3.2.tar.gz", 42 | ], 43 | sha256 = "9b9614f8a7f7b7ed93cb7975d227ece30fe7daed2c0a76f03a5ee37f69e437de", 44 | ) 45 | -------------------------------------------------------------------------------- /distroless/private/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_skylib//:bzl_library.bzl", "bzl_library") 2 | load("@rules_java//java:defs.bzl", "java_binary") 3 | 4 | exports_files([ 5 | "cacerts.sh", 6 | "locale.sh", 7 | "flatten.sh", 8 | ]) 9 | 10 | java_binary( 11 | name = "keystore_binary", 12 | srcs = ["JavaKeyStore.java"], 13 | javacopts = [ 14 | "-Xlint:-options", 15 | ], 16 | main_class = "JavaKeyStore", 17 | visibility = ["//visibility:public"], 18 | ) 19 | 20 | bzl_library( 21 | name = "cacerts", 22 | srcs = ["cacerts.bzl"], 23 | visibility = ["//distroless:__subpackages__"], 24 | deps = [":tar"], 25 | ) 26 | 27 | bzl_library( 28 | name = "locale", 29 | srcs = ["locale.bzl"], 30 | visibility = ["//distroless:__subpackages__"], 31 | deps = [":tar"], 32 | ) 33 | 34 | bzl_library( 35 | name = "group", 36 | srcs = ["group.bzl"], 37 | visibility = ["//distroless:__subpackages__"], 38 | deps = [ 39 | "@aspect_bazel_lib//lib:expand_template", 40 | "@aspect_bazel_lib//lib:tar", 41 | "@aspect_bazel_lib//lib:utils", 42 | "@bazel_skylib//rules:write_file", 43 | ], 44 | ) 45 | 46 | bzl_library( 47 | name = "os_release", 48 | srcs = ["os_release.bzl"], 49 | visibility = ["//distroless:__subpackages__"], 50 | deps = [ 51 | "@aspect_bazel_lib//lib:expand_template", 52 | "@aspect_bazel_lib//lib:tar", 53 | "@aspect_bazel_lib//lib:utils", 54 | "@bazel_skylib//rules:write_file", 55 | ], 56 | ) 57 | 58 | bzl_library( 59 | name = "passwd", 60 | srcs = ["passwd.bzl"], 61 | visibility = ["//distroless:__subpackages__"], 62 | deps = [ 63 | ":util", 64 | "@aspect_bazel_lib//lib:expand_template", 65 | "@aspect_bazel_lib//lib:tar", 66 | "@aspect_bazel_lib//lib:utils", 67 | "@bazel_skylib//rules:write_file", 68 | ], 69 | ) 70 | 71 | bzl_library( 72 | name = "java_keystore", 73 | srcs = ["java_keystore.bzl"], 74 | visibility = ["//distroless:__subpackages__"], 75 | deps = [":tar"], 76 | ) 77 | 78 | bzl_library( 79 | name = "home", 80 | srcs = ["home.bzl"], 81 | visibility = ["//distroless:__subpackages__"], 82 | deps = [ 83 | ":tar", 84 | ":util", 85 | "@aspect_bazel_lib//lib:tar", 86 | ], 87 | ) 88 | 89 | bzl_library( 90 | name = "flatten", 91 | srcs = ["flatten.bzl"], 92 | visibility = ["//distroless:__subpackages__"], 93 | deps = [":tar"], 94 | ) 95 | 96 | bzl_library( 97 | name = "tar", 98 | srcs = ["tar.bzl"], 99 | visibility = [ 100 | "//apt:__subpackages__", 101 | "//distroless:__subpackages__", 102 | ], 103 | deps = [ 104 | "@aspect_bazel_lib//lib:tar", 105 | "@bazel_skylib//lib:sets", 106 | ], 107 | ) 108 | 109 | bzl_library( 110 | name = "util", 111 | srcs = ["util.bzl"], 112 | visibility = ["//distroless:__subpackages__"], 113 | ) 114 | -------------------------------------------------------------------------------- /distroless/private/JavaKeyStore.java: -------------------------------------------------------------------------------- 1 | 2 | // Parts taken from https://github.com/openjdk/jdk17u-dev/blob/a028120220f6fd28e39fe0f6190eb1f5da6a788d/make/jdk/src/classes/build/tools/generatecacerts/GenerateCacerts.java 3 | // https://github.com/GoogleContainerTools/distroless/tree/b1e2203eceb9cc91de0500d71c648e346e1d7b89/cacerts/jksutil 4 | import java.io.DataOutputStream; 5 | import java.io.FileOutputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.io.UnsupportedEncodingException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.security.DigestOutputStream; 13 | import java.security.MessageDigest; 14 | import java.security.NoSuchAlgorithmException; 15 | import java.security.cert.Certificate; 16 | import java.security.cert.CertificateException; 17 | import java.security.cert.CertificateFactory; 18 | import java.security.cert.X509Certificate; 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | 22 | import javax.security.auth.x500.X500Principal; 23 | 24 | /** 25 | * Generate cacerts 26 | */ 27 | class JavaKeyStore { 28 | 29 | private static final int MAGIC = 0xfeedfeed; 30 | private static final int VERSION = 0x02; 31 | private static final int TRUSTED_CERT_TAG = 0x02; 32 | private static final char[] PASSWORD = "changeit".toCharArray(); 33 | private static final String SALT = "Mighty Aphrodite"; 34 | 35 | public static void main(String[] args) throws Exception { 36 | try (FileOutputStream output = new FileOutputStream(args[0])) { 37 | store(output, Arrays.copyOfRange(args, 1, args.length)); 38 | } 39 | } 40 | 41 | public static void store(OutputStream stream, String[] entries) 42 | throws IOException, NoSuchAlgorithmException, CertificateException { 43 | byte[] encoded; // the certificate encoding 44 | CertificateFactory cf = CertificateFactory.getInstance("X509"); 45 | 46 | MessageDigest md = getPreKeyedHash(PASSWORD); 47 | DataOutputStream dos = new DataOutputStream(new DigestOutputStream(stream, md)); 48 | 49 | ArrayList certs = new ArrayList(); 50 | 51 | for (String entry : entries) { 52 | try (InputStream fis = Files.newInputStream(Path.of(entry))) { 53 | for (Certificate rcert : cf.generateCertificates(fis)) { 54 | X509Certificate cert = (X509Certificate) rcert; 55 | certs.add(cert); 56 | } 57 | } 58 | } 59 | 60 | dos.writeInt(MAGIC); 61 | dos.writeInt(VERSION); 62 | dos.writeInt(certs.size()); 63 | 64 | for (X509Certificate cert : certs) { 65 | 66 | String alias = cert.getSubjectX500Principal().getName(X500Principal.CANONICAL); 67 | 68 | dos.writeInt(TRUSTED_CERT_TAG); 69 | 70 | // Write the alias 71 | dos.writeUTF(alias); 72 | 73 | // Write the (entry creation) date, which is notBefore of the cert 74 | dos.writeLong(cert.getNotBefore().getTime()); 75 | 76 | // Write the trusted certificate 77 | encoded = cert.getEncoded(); 78 | dos.writeUTF(cert.getType()); 79 | dos.writeInt(encoded.length); 80 | dos.write(encoded); 81 | } 82 | 83 | /* 84 | * Write the keyed hash which is used to detect tampering with 85 | * the keystore (such as deleting or modifying key or 86 | * certificate entries). 87 | */ 88 | byte[] digest = md.digest(); 89 | 90 | dos.write(digest); 91 | dos.flush(); 92 | } 93 | 94 | private static MessageDigest getPreKeyedHash(char[] password) 95 | throws NoSuchAlgorithmException, UnsupportedEncodingException { 96 | 97 | MessageDigest md = MessageDigest.getInstance("SHA"); 98 | byte[] passwdBytes = convertToBytes(password); 99 | md.update(passwdBytes); 100 | Arrays.fill(passwdBytes, (byte) 0x00); 101 | md.update(SALT.getBytes("UTF8")); 102 | return md; 103 | } 104 | 105 | private static byte[] convertToBytes(char[] password) { 106 | int i, j; 107 | byte[] passwdBytes = new byte[password.length * 2]; 108 | for (i = 0, j = 0; i < password.length; i++) { 109 | passwdBytes[j++] = (byte) (password[i] >> 8); 110 | passwdBytes[j++] = (byte) password[i]; 111 | } 112 | return passwdBytes; 113 | } 114 | } -------------------------------------------------------------------------------- /distroless/private/cacerts.bzl: -------------------------------------------------------------------------------- 1 | "cacerts" 2 | 3 | load(":tar.bzl", "tar_lib") 4 | 5 | _DOC = """Create a ca-certificates.crt bundle from Common CA certificates. 6 | 7 | When provided with the `ca-certificates` Debian package it will create a bundle 8 | of all common CA certificates at `/usr/share/ca-certificates` and bundle them into 9 | a `ca-certificates.crt` file at `/etc/ssl/certs/ca-certificates.crt` 10 | 11 | An example of this would be 12 | 13 | ```starlark 14 | # MODULE.bazel 15 | http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 16 | 17 | http_archive( 18 | name = "ca-certificates", 19 | type = ".deb", 20 | sha256 = "b2d488ad4d8d8adb3ba319fc9cb2cf9909fc42cb82ad239a26c570a2e749c389", 21 | urls = ["https://snapshot.debian.org/archive/debian/20231106T210201Z/pool/main/c/ca-certificates/ca-certificates_20210119_all.deb"], 22 | build_file_content = "exports_files(["data.tar.xz"])" 23 | ) 24 | 25 | # BUILD.bazel 26 | load("@rules_distroless//distroless:defs.bzl", "cacerts") 27 | 28 | cacerts( 29 | name = "example", 30 | package = "@ca-certificates//:data.tar.xz", 31 | ) 32 | ``` 33 | 34 | To use the generated certificate bundle for SSL, **you must set SSL_CERT_FILE in the 35 | environment**. You can set it on the oci image like so: 36 | ```starlark 37 | oci_image( 38 | name = "my-image", 39 | env = { 40 | "SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt", 41 | } 42 | ) 43 | ``` 44 | """ 45 | 46 | def _cacerts_impl(ctx): 47 | bsdtar = ctx.toolchains[tar_lib.TOOLCHAIN_TYPE] 48 | 49 | cacerts = ctx.actions.declare_file(ctx.attr.name + ".crt") 50 | copyright = ctx.actions.declare_file(ctx.attr.name + ".copyright") 51 | ctx.actions.run( 52 | executable = ctx.executable._cacerts_sh, 53 | inputs = [ctx.file.package], 54 | outputs = [cacerts, copyright], 55 | tools = bsdtar.default.files, 56 | arguments = [ 57 | bsdtar.tarinfo.binary.path, 58 | ctx.file.package.path, 59 | cacerts.path, 60 | copyright.path, 61 | ], 62 | ) 63 | 64 | output = ctx.actions.declare_file(ctx.attr.name + ".tar.gz") 65 | mtree = tar_lib.create_mtree(ctx) 66 | 67 | # TODO: We should have a rule `rootfs` that creates the filesystem root. 68 | # We'll add this for now to match distroless images. 69 | mtree.add_dir("/etc", mode = "0755", time = ctx.attr.time) 70 | mtree.add_parents("/etc/ssl/certs", mode = "0755", time = ctx.attr.time, skip = [1]) 71 | mtree.add_file("/etc/ssl/certs/ca-certificates.crt", cacerts, time = ctx.attr.time, mode = ctx.attr.mode) 72 | mtree.add_parents("/usr/share/doc/ca-certificates", time = ctx.attr.time) 73 | mtree.add_file("/usr/share/doc/ca-certificates/copyright", copyright, time = ctx.attr.time, mode = ctx.attr.mode) 74 | mtree.build(output = output, mnemonic = "CaCertsTarGz", inputs = [cacerts, copyright]) 75 | 76 | return [ 77 | DefaultInfo(files = depset([output])), 78 | ] 79 | 80 | cacerts = rule( 81 | doc = _DOC, 82 | attrs = { 83 | "_cacerts_sh": attr.label( 84 | allow_single_file = True, 85 | executable = True, 86 | cfg = "exec", 87 | default = ":cacerts.sh", 88 | ), 89 | "package": attr.label( 90 | allow_single_file = [".tar.zst", ".tar.xz", ".tar.gz", ".tar"], 91 | mandatory = True, 92 | ), 93 | "mode": attr.string( 94 | doc = "mode for the entries", 95 | default = "0555", 96 | ), 97 | "time": attr.string( 98 | doc = "time for the entries", 99 | default = "0.0", 100 | ), 101 | }, 102 | implementation = _cacerts_impl, 103 | toolchains = [tar_lib.TOOLCHAIN_TYPE], 104 | ) 105 | -------------------------------------------------------------------------------- /distroless/private/cacerts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o pipefail -o errexit -o nounset 3 | 4 | readonly bsdtar="$1" 5 | readonly package_path="$2" 6 | readonly cacerts_out="$3" 7 | readonly copyright_out="$4" 8 | readonly tmp="$(mktemp -d)" 9 | 10 | "$bsdtar" -xf "$package_path" -C "$tmp" ./usr/share/ca-certificates ./usr/share/doc/ca-certificates/copyright 11 | 12 | mv "$tmp/usr/share/doc/ca-certificates/copyright" "$copyright_out" 13 | 14 | function add_cert () { 15 | local dir="$1" 16 | 17 | if test -d "${dir}"; then 18 | for cert in "${dir}"/*; do 19 | if test -d "${cert}"; then 20 | add_cert "${cert}" 21 | continue 22 | fi 23 | while IFS= read -r IN; do 24 | printf "%s\n" "${IN}" >> $cacerts_out 25 | done <"${cert}" 26 | done 27 | fi 28 | } 29 | 30 | add_cert "$tmp/usr/share/ca-certificates" 31 | rm -rf "$tmp" 32 | -------------------------------------------------------------------------------- /distroless/private/flatten.bzl: -------------------------------------------------------------------------------- 1 | "flatten" 2 | 3 | load(":tar.bzl", "tar_lib") 4 | 5 | _DOC = """Flatten multiple archives into single archive.""" 6 | 7 | def _flatten_impl(ctx): 8 | bsdtar = ctx.toolchains[tar_lib.TOOLCHAIN_TYPE] 9 | 10 | ext = tar_lib.common.compression_to_extension[ctx.attr.compress] if ctx.attr.compress else ".tar" 11 | output = ctx.actions.declare_file(ctx.attr.name + ext) 12 | 13 | args = ctx.actions.args() 14 | args.add(bsdtar.tarinfo.binary) 15 | args.add(str(ctx.attr.deduplicate)) 16 | args.add_all(tar_lib.DEFAULT_ARGS) 17 | args.add("--create") 18 | tar_lib.common.add_compression_args(ctx.attr.compress, args) 19 | args.add("--file", output) 20 | args.add_all(ctx.files.tars, format_each = "@%s") 21 | 22 | ctx.actions.run( 23 | executable = ctx.executable._flatten_sh, 24 | inputs = ctx.files.tars, 25 | outputs = [output], 26 | tools = bsdtar.default.files, 27 | arguments = [args], 28 | mnemonic = "Flatten", 29 | progress_message = "Flattening %{label}", 30 | ) 31 | 32 | return [ 33 | DefaultInfo(files = depset([output])), 34 | ] 35 | 36 | flatten = rule( 37 | doc = _DOC, 38 | attrs = { 39 | "tars": attr.label_list( 40 | allow_files = tar_lib.common.accepted_tar_extensions, 41 | mandatory = True, 42 | allow_empty = False, 43 | doc = "List of tars to flatten", 44 | ), 45 | "deduplicate": attr.bool(doc = """\ 46 | EXPERIMENTAL: We may change or remove it without a notice. 47 | 48 | Remove duplicate entries from the archives after flattening. 49 | Deduplication is performed only for directories. 50 | 51 | This requires `awk` to be available in the PATH. 52 | """, default = False), 53 | "compress": attr.string( 54 | doc = "Compress the archive file with a supported algorithm.", 55 | values = tar_lib.common.accepted_compression_types, 56 | ), 57 | "_flatten_sh": attr.label(default = "//distroless/private:flatten.sh", executable = True, cfg = "exec", allow_single_file = True), 58 | }, 59 | implementation = _flatten_impl, 60 | toolchains = [tar_lib.TOOLCHAIN_TYPE], 61 | ) 62 | -------------------------------------------------------------------------------- /distroless/private/flatten.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o pipefail -o errexit 3 | 4 | bsdtar="$1"; 5 | deduplicate="$2"; 6 | shift 2; 7 | 8 | # Deduplication requested, use this complex pipeline to deduplicate. 9 | if [[ "$deduplicate" == "True" ]]; then 10 | 11 | mtree=$(mktemp) 12 | 13 | # List files in all archives and append to single column mtree. 14 | for arg in "$@"; do 15 | if [[ "$arg" == "@"* ]]; then 16 | "$bsdtar" -tf "${arg:1}" >> "$mtree" 17 | fi 18 | done 19 | 20 | # There not a lot happening here but there is still too many implicit knowledge. 21 | # 22 | # When we run bsdtar, we ask for it to prompt every entry, in the same order we created above, the mtree. 23 | # See: https://github.com/libarchive/libarchive/blob/f745a848d7a81758cd9fcd49d7fd45caeebe1c3d/tar/write.c#L683 24 | # 25 | # For every prompt, therefore entry, we have write 31 bytes of data, one of which has to be either 'Y' or 'N'. 26 | # And the reason for it is that since we are not TTY and pretending to be one, we can't interleave write calls 27 | # so we have to interleave it by filling up the buffer with 31 bytes of 'Y' or 'N'. 28 | # See: https://github.com/libarchive/libarchive/blob/f745a848d7a81758cd9fcd49d7fd45caeebe1c3d/tar/util.c#L240 29 | # See: https://github.com/libarchive/libarchive/blob/f745a848d7a81758cd9fcd49d7fd45caeebe1c3d/tar/util.c#L216 30 | # 31 | # To match the extraction behavior of tar itself, we want to preserve only the final occurrence of each file 32 | # and directory in the archive. To do this, we iterate over all the entries twice. The first pass computes the 33 | # number of occurrences of each path, and the second pass determines whether each entry is the final (or only) 34 | # occurrence of that path. 35 | 36 | $bsdtar --confirmation "$@" 2< <(awk '{ 37 | count[$1]++; 38 | files[NR] = $1 39 | } 40 | END { 41 | ORS="" 42 | for (i=1; i<=NR; i++) { 43 | seen[files[i]]++ 44 | keep="n" 45 | if (count[files[i]] == seen[files[i]]) { 46 | keep="y" 47 | } 48 | for (j=0; j<31; j++) print keep 49 | fflush() 50 | } 51 | }' "$mtree") 52 | rm "$mtree" 53 | else 54 | # No deduplication, business as usual 55 | $bsdtar "$@" 56 | fi -------------------------------------------------------------------------------- /distroless/private/group.bzl: -------------------------------------------------------------------------------- 1 | "group" 2 | 3 | load("@aspect_bazel_lib//lib:tar.bzl", "tar") 4 | load("@aspect_bazel_lib//lib:utils.bzl", "propagate_common_rule_attributes") 5 | load("@bazel_skylib//rules:write_file.bzl", "write_file") 6 | load(":tar.bzl", "tar_lib") 7 | load(":util.bzl", "util") 8 | 9 | def group(name, entries, time = "0.0", mode = "0644", **kwargs): 10 | """ 11 | Create a group file from array of dicts. 12 | 13 | https://www.ibm.com/docs/en/aix/7.2?topic=files-etcgroup-file#group_security__a21597b8__title__1 14 | 15 | Args: 16 | name: name of the target 17 | entries: an array of dicts which will be serialized into single group file. 18 | mode: mode for the entry 19 | time: time for the entry 20 | **kwargs: other named arguments to expanded targets. see [common rule attributes](https://bazel.build/reference/be/common-definitions#common-attributes). 21 | """ 22 | common_kwargs = propagate_common_rule_attributes(kwargs) 23 | write_file( 24 | name = "%s_content" % name, 25 | content = [ 26 | # See https://www.ibm.com/docs/en/aix/7.2?topic=files-etcgroup-file#group_security__a3179518__title__1 27 | ":".join([ 28 | util.get_attr(entry, "name"), 29 | util.get_attr(entry, "password", "!"), # not used. Group administrators are provided instead of group passwords. 30 | str(util.get_attr(entry, "gid")), 31 | ",".join(util.get_attr(entry, "users", [])), 32 | ]) 33 | for entry in entries 34 | ] + [""], 35 | out = "%s.content" % name, 36 | **common_kwargs 37 | ) 38 | 39 | mtree = tar_lib.create_mtree() 40 | 41 | # TODO: We should have a rule `rootfs` that creates the filesystem root. 42 | # We'll add this for now to match distroless images. 43 | mtree.add_dir("/etc", mode = "0755", time = time) 44 | mtree.entry( 45 | "/etc/group", 46 | "file", 47 | mode = mode, 48 | time = time, 49 | content = "$(BINDIR)/$(rootpath :%s_content)" % name, 50 | ) 51 | 52 | tar( 53 | name = name, 54 | srcs = [":%s_content" % name], 55 | mtree = mtree.content(), 56 | args = tar_lib.DEFAULT_ARGS, 57 | compress = "gzip", 58 | **common_kwargs 59 | ) 60 | -------------------------------------------------------------------------------- /distroless/private/home.bzl: -------------------------------------------------------------------------------- 1 | "home" 2 | 3 | load("@aspect_bazel_lib//lib:tar.bzl", "tar") 4 | load(":tar.bzl", "tar_lib") 5 | load(":util.bzl", "util") 6 | 7 | def home(name, dirs, **kwargs): 8 | """ 9 | Create home directories with specific uid and gids. 10 | 11 | Args: 12 | name: name of the target 13 | dirs: array of home directory dicts. 14 | **kwargs: other named arguments to that is passed to tar. see [common rule attributes](https://bazel.build/reference/be/common-definitions#common-attributes). 15 | """ 16 | mtree = tar_lib.create_mtree() 17 | 18 | for home in dirs: 19 | mtree.add_dir( 20 | util.get_attr(home, "home"), 21 | uid = str(util.get_attr(home, "uid")), 22 | gid = str(util.get_attr(home, "gid")), 23 | time = str(util.get_attr(home, "time", 0)), 24 | # the default matches https://github.com/bazelbuild/rules_docker/blob/3040e1fd74659a52d1cdaff81359f57ee0e2bb41/contrib/passwd.bzl#L81C24-L81C27 25 | mode = str(util.get_attr(home, "mode", "700")), 26 | ) 27 | 28 | tar( 29 | name = name, 30 | mtree = mtree.content(), 31 | args = tar_lib.DEFAULT_ARGS, 32 | compress = "gzip", 33 | **kwargs 34 | ) 35 | -------------------------------------------------------------------------------- /distroless/private/java_keystore.bzl: -------------------------------------------------------------------------------- 1 | "jks" 2 | 3 | load(":tar.bzl", "tar_lib") 4 | 5 | _DOC = """Create a java keystore (database) of cryptographic keys, X.509 certificate chains, and trusted certificates. 6 | 7 | Currently only public X.509 are supported as part of the PUBLIC API contract. 8 | """ 9 | 10 | def _java_keystore_impl(ctx): 11 | jks = ctx.actions.declare_file(ctx.attr.name + ".jks") 12 | 13 | args = ctx.actions.args() 14 | args.add(jks) 15 | args.add_all(ctx.files.certificates) 16 | 17 | ctx.actions.run( 18 | executable = ctx.executable._java_keystore, 19 | inputs = ctx.files.certificates, 20 | outputs = [jks], 21 | arguments = [args], 22 | ) 23 | 24 | output = ctx.actions.declare_file(ctx.attr.name + ".tar.gz") 25 | mtree = tar_lib.create_mtree(ctx) 26 | 27 | # TODO: We should have a rule `rootfs` that creates the filesystem root. 28 | # We'll add this for now to match distroless images. 29 | mtree.add_dir("/etc", mode = ctx.attr.mode, time = "946684800") 30 | mtree.add_parents("/etc/ssl/certs/java", mode = ctx.attr.mode, time = ctx.attr.time, skip = [1]) 31 | 32 | # TODO: remove?? 33 | mtree.add_file("/etc/ssl/certs/java/cacerts", jks, mode = "0555", time = ctx.attr.time) 34 | mtree.build(output = output, mnemonic = "JavaKeyStore", inputs = [jks]) 35 | 36 | return [ 37 | DefaultInfo(files = depset([output])), 38 | OutputGroupInfo( 39 | jks = depset([jks]), 40 | ), 41 | ] 42 | 43 | java_keystore = rule( 44 | doc = _DOC, 45 | attrs = { 46 | "_java_keystore": attr.label( 47 | executable = True, 48 | cfg = "exec", 49 | default = ":keystore_binary", 50 | ), 51 | "certificates": attr.label_list( 52 | allow_files = True, 53 | mandatory = True, 54 | allow_empty = False, 55 | ), 56 | "mode": attr.string( 57 | doc = "mode for the entries", 58 | default = "0755", 59 | ), 60 | "time": attr.string( 61 | doc = "time for the entries", 62 | default = "0.0", 63 | ), 64 | }, 65 | implementation = _java_keystore_impl, 66 | toolchains = [ 67 | tar_lib.TOOLCHAIN_TYPE, 68 | ], 69 | ) 70 | -------------------------------------------------------------------------------- /distroless/private/locale.bzl: -------------------------------------------------------------------------------- 1 | "locale" 2 | 3 | load(":tar.bzl", "tar_lib") 4 | 5 | _DOC = """Create a locale archive from a Debian package. 6 | 7 | An example of this would be 8 | 9 | ```starlark 10 | # MODULE.bazel 11 | http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 12 | 13 | http_archive( 14 | name = "libc-bin", 15 | build_file_content = 'exports_files(["data.tar.xz"])', 16 | sha256 = "8b048ab5c7e9f5b7444655541230e689631fd9855c384e8c4a802586d9bbc65a", 17 | urls = ["https://snapshot.debian.org/archive/debian-security/20231106T230332Z/pool/updates/main/g/glibc/libc-bin_2.31-13+deb11u7_amd64.deb"], 18 | ) 19 | 20 | # BUILD.bazel 21 | load("@rules_distroless//distroless:defs.bzl", "locale") 22 | 23 | locale( 24 | name = "example", 25 | package = "@libc-bin//:data.tar.xz" 26 | ) 27 | ``` 28 | """ 29 | 30 | def _locale_impl(ctx): 31 | bsdtar = ctx.toolchains[tar_lib.TOOLCHAIN_TYPE] 32 | 33 | output = ctx.actions.declare_file(ctx.attr.name + ".tar.gz") 34 | 35 | args = ctx.actions.args() 36 | 37 | args.add(bsdtar.tarinfo.binary) 38 | args.add(output) 39 | args.add(ctx.file.package) 40 | args.add(ctx.attr.time) 41 | args.add("--include", "^./usr/$") 42 | args.add("--include", "^./usr/lib/$") 43 | args.add("--include", "^./usr/lib/locale/$") 44 | args.add("--include", "./usr/lib/locale/%s" % ctx.attr.charset) 45 | args.add("--include", "^./usr/share/$") 46 | args.add("--include", "^./usr/share/doc/$") 47 | args.add("--include", "^./usr/share/doc/libc-bin/$") 48 | args.add("--include", "^./usr/share/doc/libc-bin/copyright$") 49 | 50 | ctx.actions.run( 51 | executable = ctx.executable._locale_sh, 52 | inputs = [ctx.file.package], 53 | outputs = [output], 54 | tools = bsdtar.default.files, 55 | arguments = [args], 56 | ) 57 | return [ 58 | DefaultInfo(files = depset([output])), 59 | ] 60 | 61 | locale = rule( 62 | doc = _DOC, 63 | attrs = { 64 | "_locale_sh": attr.label( 65 | allow_single_file = True, 66 | executable = True, 67 | cfg = "exec", 68 | default = ":locale.sh", 69 | ), 70 | "package": attr.label( 71 | allow_single_file = [".tar.xz", ".tar.gz", ".tar"], 72 | mandatory = True, 73 | ), 74 | "charset": attr.string( 75 | default = "C.utf8", 76 | ), 77 | "time": attr.string( 78 | doc = "time for the entries", 79 | default = "0.0", 80 | ), 81 | }, 82 | implementation = _locale_impl, 83 | toolchains = [tar_lib.TOOLCHAIN_TYPE], 84 | ) 85 | -------------------------------------------------------------------------------- /distroless/private/locale.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o pipefail -o errexit -o nounset 3 | 4 | readonly bsdtar="$1" 5 | readonly out="$2" 6 | readonly package_path="$3" 7 | readonly time="$4" 8 | shift 4 9 | 10 | # TODO: there must be a better way to manipulate tars! 11 | # "$bsdtar" -cf $out --posix --no-same-owner --options="" $@ "@$package_path" 12 | # "$bsdtar" -cf to.mtree $@ --format=mtree --options '!gname,!uname,!sha1,!nlink' "@$package_path" 13 | # "$bsdtar" --older "0" -Uf $out @to.mtree 14 | 15 | tmp=$(mktemp -d) 16 | "$bsdtar" -xf "$package_path" $@ -C "$tmp" 17 | "$bsdtar" -cf - $@ --format=mtree --options '!gname,!uname,!sha1,!nlink,!time' "@$package_path" | 18 | sed 's/$/ time='"$time"'/' | 19 | "$bsdtar" --gzip --options 'gzip:!timestamp' -cf "$out" -C "$tmp/" @- 20 | -------------------------------------------------------------------------------- /distroless/private/os_release.bzl: -------------------------------------------------------------------------------- 1 | "os release" 2 | 3 | load("@aspect_bazel_lib//lib:tar.bzl", "tar") 4 | load("@aspect_bazel_lib//lib:utils.bzl", "propagate_common_rule_attributes") 5 | load("@bazel_skylib//rules:write_file.bzl", "write_file") 6 | load(":tar.bzl", "tar_lib") 7 | 8 | def os_release( 9 | name, 10 | content, 11 | path = "/usr/lib/os-release", 12 | mode = "0555", 13 | time = "0", 14 | **kwargs): 15 | """ 16 | Create an Operating System Identification file from a key, value dictionary. 17 | 18 | https://www.freedesktop.org/software/systemd/man/latest/os-release.html 19 | 20 | Args: 21 | name: name of the target 22 | content: a key, value dictionary that will be serialized into `=` seperated lines. 23 | 24 | See https://www.freedesktop.org/software/systemd/man/latest/os-release.html#Options for well known keys. 25 | path: where to put the file in the result archive. default: `/usr/lib/os-release` 26 | mode: mode for the entry 27 | time: time for the entry 28 | **kwargs: other named arguments to expanded targets. see [common rule attributes](https://bazel.build/reference/be/common-definitions#common-attributes). 29 | """ 30 | common_kwargs = propagate_common_rule_attributes(kwargs) 31 | write_file( 32 | name = "%s_content" % name, 33 | content = [ 34 | "{}={}".format(key, value) 35 | for (key, value) in content.items() 36 | ] + [""], 37 | out = "%s.content" % name, 38 | **common_kwargs 39 | ) 40 | 41 | mtree = tar_lib.create_mtree() 42 | 43 | i = path.rfind("/") 44 | mtree.add_parents(path[0:i], time = time) 45 | mtree.entry( 46 | path.lstrip("/").lstrip("./"), 47 | "file", 48 | mode = mode, 49 | time = time, 50 | content = "$(BINDIR)/$(rootpath :%s_content)" % name, 51 | ) 52 | 53 | tar( 54 | name = name, 55 | srcs = [":%s_content" % name], 56 | mtree = mtree.content(), 57 | args = tar_lib.DEFAULT_ARGS, 58 | compress = "gzip", 59 | **common_kwargs 60 | ) 61 | -------------------------------------------------------------------------------- /distroless/private/passwd.bzl: -------------------------------------------------------------------------------- 1 | "osrelease" 2 | 3 | load("@aspect_bazel_lib//lib:tar.bzl", "tar") 4 | load("@aspect_bazel_lib//lib:utils.bzl", "propagate_common_rule_attributes") 5 | load("@bazel_skylib//rules:write_file.bzl", "write_file") 6 | load(":tar.bzl", "tar_lib") 7 | 8 | # WARNING: the mode `0o644` is important 9 | # See: https://github.com/bazelbuild/rules_docker/blob/3040e1fd74659a52d1cdaff81359f57ee0e2bb41/contrib/passwd.bzl#L149C54-L149C57 10 | def passwd(name, entries, mode = "0644", time = "0.0", **kwargs): 11 | """ 12 | Create a passwd file from array of dicts. 13 | 14 | https://www.ibm.com/docs/en/aix/7.3?topic=passwords-using-etcpasswd-file 15 | 16 | Args: 17 | name: name of the target 18 | entries: an array of dicts which will be serialized into single passwd file. 19 | 20 | An example; 21 | 22 | ``` 23 | dict(gid = 0, uid = 0, home = "/root", shell = "/bin/bash", username = "root") 24 | ``` 25 | mode: mode for the entry 26 | time: time for the entry 27 | **kwargs: other named arguments to expanded targets. see [common rule attributes](https://bazel.build/reference/be/common-definitions#common-attributes). 28 | """ 29 | common_kwargs = propagate_common_rule_attributes(kwargs) 30 | write_file( 31 | name = "%s_content" % name, 32 | content = [ 33 | # See: https://www.ibm.com/docs/kk/aix/7.2?topic=files-etcpasswd-file#passwd_security__a21597b8__title__1 34 | ":".join([ 35 | entry["username"], 36 | entry.pop("password", "!"), 37 | str(entry["uid"]), 38 | str(entry["gid"]), 39 | ",".join(entry.pop("gecos", [])), 40 | entry["home"], 41 | entry["shell"], 42 | ]) 43 | for entry in entries 44 | ] + [""], 45 | out = "%s.content" % name, 46 | **common_kwargs 47 | ) 48 | 49 | mtree = tar_lib.create_mtree() 50 | 51 | # TODO: We should have a rule `rootfs` that creates the filesystem root. 52 | # We'll add this for now to match distroless images. 53 | mtree.add_dir("/etc", mode = "0755", time = "0.0") 54 | mtree.entry( 55 | "/etc/passwd", 56 | "file", 57 | mode = mode, 58 | time = time, 59 | content = "$(BINDIR)/$(rootpath :%s_content)" % name, 60 | ) 61 | 62 | tar( 63 | name = name, 64 | srcs = [":%s_content" % name], 65 | mtree = mtree.content(), 66 | args = tar_lib.DEFAULT_ARGS, 67 | compress = "gzip", 68 | **common_kwargs 69 | ) 70 | -------------------------------------------------------------------------------- /distroless/private/tar.bzl: -------------------------------------------------------------------------------- 1 | "mtree helpers" 2 | 3 | load("@aspect_bazel_lib//lib:tar.bzl", tar = "tar_lib") 4 | load("@bazel_skylib//lib:sets.bzl", "sets") 5 | 6 | DEFAULT_GID = "0" 7 | DEFAULT_UID = "0" 8 | DEFAULT_TIME = "0.0" 9 | DEFAULT_MODE = "0755" 10 | DEFAULT_ARGS = [ 11 | # TODO: distroless uses gnu archives 12 | "--format", 13 | "gnutar", 14 | ] 15 | 16 | def _mtree_line(dest, type, content = None, uid = DEFAULT_UID, gid = DEFAULT_GID, time = DEFAULT_TIME, mode = DEFAULT_MODE): 17 | # mtree expects paths to start with ./ so normalize paths that starts with 18 | # `/` or relative path (without / and ./) 19 | if not dest.startswith("."): 20 | if not dest.startswith("/"): 21 | dest = "/" + dest 22 | dest = "." + dest 23 | 24 | spec = [ 25 | dest, 26 | "uid=" + uid, 27 | "gid=" + gid, 28 | "time=" + time, 29 | "mode=" + mode, 30 | "type=" + type, 31 | ] 32 | if content: 33 | spec.append("content=" + content) 34 | return " ".join(spec) 35 | 36 | def _add_parents(path, uid = DEFAULT_UID, gid = DEFAULT_GID, time = DEFAULT_TIME, mode = DEFAULT_MODE, skip = []): 37 | lines = [] 38 | segments = path.split("/") 39 | for i in range(0, len(segments)): 40 | parent = "/".join(segments[:i + 1]) 41 | if not parent or i in skip: 42 | continue 43 | lines.append( 44 | _mtree_line(parent, "dir", uid = uid, gid = gid, time = time, mode = mode), 45 | ) 46 | return lines 47 | 48 | def _build_tar(ctx, mtree, output, inputs = [], compression = "gzip", mnemonic = "Tar"): 49 | bsdtar = ctx.toolchains[tar.toolchain_type] 50 | 51 | inputs = inputs[:] 52 | inputs.append(mtree) 53 | 54 | args = ctx.actions.args() 55 | args.add_all(DEFAULT_ARGS) 56 | args.add("--create") 57 | tar.common.add_compression_args(compression, args) 58 | args.add("--file", output) 59 | args.add(mtree, format = "@%s") 60 | 61 | ctx.actions.run( 62 | executable = bsdtar.tarinfo.binary, 63 | inputs = inputs, 64 | outputs = [output], 65 | tools = bsdtar.default.files, 66 | arguments = [args], 67 | mnemonic = mnemonic, 68 | ) 69 | 70 | def _build_mtree(ctx, content): 71 | mtree_out = ctx.actions.declare_file(ctx.label.name + ".spec") 72 | content.add("#end") 73 | ctx.actions.write(mtree_out, content = content) 74 | return mtree_out 75 | 76 | def _array_content(): 77 | content = [] 78 | return struct( 79 | add = lambda c: content.append(c), 80 | add_all = lambda c, uniquify = False: content.extend(c) if uniquify else content.extend(sets.make(c).to_list()), 81 | to_list = lambda: content, 82 | ) 83 | 84 | def _create_mtree(ctx = None): 85 | if ctx: 86 | content = ctx.actions.args() 87 | content.set_param_file_format("multiline") 88 | else: 89 | content = _array_content() 90 | 91 | content.add("#mtree") 92 | return struct( 93 | entry = lambda path, type, **kwargs: content.add(_mtree_line(path, type, **kwargs)), 94 | add_file = lambda path, file, **kwargs: content.add(_mtree_line(path, "file", content = file.path, **kwargs)), 95 | add_dir = lambda path, **kwargs: content.add(_mtree_line(path, "dir", **kwargs)), 96 | add_parents = lambda path, **kwargs: content.add_all(_add_parents(path, **kwargs), uniquify = True), 97 | build = lambda **kwargs: _build_tar(ctx, _build_mtree(ctx, content), **kwargs), 98 | content = lambda: content.to_list() + ["#end"], 99 | ) 100 | 101 | tar_lib = struct( 102 | TOOLCHAIN_TYPE = tar.toolchain_type, 103 | DEFAULT_ARGS = DEFAULT_ARGS, 104 | create_mtree = _create_mtree, 105 | common = tar.common, 106 | ) 107 | -------------------------------------------------------------------------------- /distroless/private/util.bzl: -------------------------------------------------------------------------------- 1 | "util" 2 | 3 | def _get_attr(o, k, d = None): 4 | if k in o: 5 | return o[k] 6 | if hasattr(o, k): 7 | return getattr(o, k) 8 | if d != None: 9 | return d 10 | fail("missing key %s" % k) 11 | 12 | util = struct( 13 | get_attr = _get_attr, 14 | ) 15 | -------------------------------------------------------------------------------- /distroless/tests/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_skylib//:bzl_library.bzl", "bzl_library") 2 | 3 | bzl_library( 4 | name = "asserts", 5 | srcs = ["asserts.bzl"], 6 | visibility = ["//visibility:public"], 7 | deps = [ 8 | "@aspect_bazel_lib//lib:diff_test", 9 | "@bazel_skylib//rules:write_file", 10 | ], 11 | ) 12 | -------------------------------------------------------------------------------- /distroless/tests/asserts.bzl: -------------------------------------------------------------------------------- 1 | "Make shorter assertions" 2 | 3 | load("@aspect_bazel_lib//lib:diff_test.bzl", "diff_test") 4 | load("@bazel_skylib//rules:write_file.bzl", "write_file") 5 | 6 | def assert_tar_mtree(name, actual, expected): 7 | """ 8 | Assert that an mtree representation of a tarball matches an expected value. 9 | 10 | Args: 11 | name: name of this assertion 12 | actual: label for a tarball 13 | expected: expected mtree 14 | """ 15 | actual_mtree = "_{}_mtree".format(name) 16 | expected_mtree = "_{}_expected".format(name) 17 | 18 | native.genrule( 19 | name = actual_mtree, 20 | srcs = [actual], 21 | outs = ["_{}.mtree".format(name)], 22 | cmd = "cat $(execpath {}) | $(BSDTAR_BIN) -cf $@ --format=mtree --options '!nlink' @-".format(actual), 23 | toolchains = ["@bsd_tar_toolchains//:resolved_toolchain"], 24 | ) 25 | 26 | write_file( 27 | name = expected_mtree, 28 | out = "_{}.expected".format(name), 29 | content = [expected], 30 | newline = "unix", 31 | ) 32 | 33 | diff_test( 34 | name = name, 35 | file1 = actual_mtree, 36 | file2 = expected_mtree, 37 | timeout = "short", 38 | ) 39 | 40 | def assert_tar_listing(name, actual, expected): 41 | """ 42 | Assert that the listed contents of a tarball match an expected value. This is useful when checking for duplicated paths. 43 | 44 | Args: 45 | name: name of this assertion 46 | actual: label for a tarball 47 | expected: expected listing 48 | """ 49 | actual_listing = "_{}_listing".format(name) 50 | expected_listing = "_{}_expected".format(name) 51 | 52 | native.genrule( 53 | name = actual_listing, 54 | srcs = [actual], 55 | outs = ["_{}.listing".format(name)], 56 | cmd = "cat $(execpath {}) | $(BSDTAR_BIN) -tf - > $@".format(actual), 57 | toolchains = ["@bsd_tar_toolchains//:resolved_toolchain"], 58 | ) 59 | 60 | write_file( 61 | name = expected_listing, 62 | out = "_{}.expected".format(name), 63 | content = [expected], 64 | newline = "unix", 65 | ) 66 | 67 | diff_test( 68 | name = name, 69 | file1 = actual_listing, 70 | file2 = expected_listing, 71 | timeout = "short", 72 | ) 73 | 74 | # buildifier: disable=function-docstring 75 | def assert_jks_listing(name, actual, expected): 76 | actual_listing = "_{}_listing".format(name) 77 | 78 | native.genrule( 79 | name = actual_listing, 80 | srcs = [ 81 | actual, 82 | "@rules_java//toolchains:current_java_runtime", 83 | ], 84 | outs = ["_{}.listing".format(name)], 85 | cmd = """ 86 | #!/usr/bin/env bash 87 | set -o pipefail -o errexit -o nounset 88 | 89 | BINS=($(locations @rules_java//toolchains:current_java_runtime)) 90 | KEYTOOL=$$(dirname $${BINS[1]})/keytool 91 | 92 | $$KEYTOOL -J-Duser.language=en -J-Duser.country=US -J-Duser.timezone=UTC \\ 93 | -list -rfc -keystore $(location %s) -storepass changeit > $@ 94 | """ % actual, 95 | ) 96 | 97 | diff_test( 98 | name = name, 99 | file1 = actual_listing, 100 | file2 = expected, 101 | timeout = "short", 102 | ) 103 | -------------------------------------------------------------------------------- /distroless/toolchains.bzl: -------------------------------------------------------------------------------- 1 | "macro for registering toolchains required" 2 | 3 | load("@aspect_bazel_lib//lib:repositories.bzl", "register_expand_template_toolchains", "register_tar_toolchains", "register_yq_toolchains", "register_zstd_toolchains") 4 | load("@rules_java//java:repositories.bzl", "rules_java_toolchains") 5 | load("@rules_java//java:rules_java_deps.bzl", "rules_java_dependencies") 6 | 7 | def distroless_register_toolchains(): 8 | """Register all toolchains required by distroless.""" 9 | register_yq_toolchains() 10 | register_zstd_toolchains() 11 | register_tar_toolchains() 12 | register_expand_template_toolchains() 13 | rules_java_dependencies() 14 | rules_java_toolchains() 15 | -------------------------------------------------------------------------------- /docs/.bazelrc: -------------------------------------------------------------------------------- 1 | # Never Compile protoc Again 2 | # Don't build protoc from the cc_binary, it's slow and spammy when cache miss 3 | common --per_file_copt=external/.*protobuf.*@--PROTOBUF_WAS_NOT_SUPPOSED_TO_BE_BUILT 4 | common --host_per_file_copt=external/.*protobuf.*@--PROTOBUF_WAS_NOT_SUPPOSED_TO_BE_BUILT 5 | common --java_runtime_version=remotejdk_11 6 | test --test_output=errors 7 | -------------------------------------------------------------------------------- /docs/.bazelversion: -------------------------------------------------------------------------------- 1 | 8.0.0 2 | -------------------------------------------------------------------------------- /docs/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@aspect_bazel_lib//lib:docs.bzl", "stardoc_with_diff_test", "update_docs") 2 | load("@rules_java//java:java_binary.bzl", "java_binary") 3 | 4 | java_binary( 5 | name = "renderer", 6 | main_class = "com/google/devtools/build/stardoc/renderer/RendererMain", 7 | runtime_deps = ["@stardoc-prebuilt//jar"], 8 | ) 9 | 10 | stardoc_with_diff_test( 11 | name = "rules", 12 | bzl_library_target = "@rules_distroless//distroless:defs", 13 | renderer = "renderer", 14 | ) 15 | 16 | stardoc_with_diff_test( 17 | name = "apt", 18 | bzl_library_target = "@rules_distroless//apt:extensions", 19 | renderer = "renderer", 20 | ) 21 | 22 | stardoc_with_diff_test( 23 | name = "apt_macro", 24 | bzl_library_target = "@rules_distroless//apt:apt", 25 | renderer = "renderer", 26 | ) 27 | 28 | update_docs(name = "update") 29 | -------------------------------------------------------------------------------- /docs/MODULE.bazel: -------------------------------------------------------------------------------- 1 | bazel_dep(name = "aspect_bazel_lib", version = "2.14.0") 2 | bazel_dep(name = "rules_distroless", version = "0.0.0") 3 | bazel_dep(name = "rules_java", version = "8.12.0") 4 | bazel_dep(name = "stardoc", version = "0.7.1", repo_name = "io_bazel_stardoc") 5 | bazel_dep(name = "platforms", version = "1.0.0") 6 | 7 | local_path_override( 8 | module_name = "rules_distroless", 9 | path = "..", 10 | ) 11 | 12 | http_jar = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_jar") 13 | 14 | http_jar( 15 | name = "stardoc-prebuilt", 16 | integrity = "sha256-jDi5ITmziwwiHCsfd8v0UOoraWXIAfICIll+wbpg/vE=", 17 | urls = ["https://github.com/alexeagle/stardoc-prebuilt/releases/download/v0.7.1/renderer_deploy.jar"], 18 | ) 19 | -------------------------------------------------------------------------------- /docs/apt.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | apt extensions 4 | 5 | 6 | 7 | ## apt 8 | 9 |
 10 | apt = use_extension("@rules_distroless//apt:extensions.bzl", "apt")
 11 | apt.install(lock, manifest, mergedusr, nolock, package_template, resolve_transitive)
 12 | 
13 | 14 | 15 | **TAG CLASSES** 16 | 17 | 18 | 19 | ### install 20 | 21 | Module extension to create Debian repositories. 22 | 23 | Create Debian repositories with packages "installed" in them and available 24 | to use in Bazel. 25 | 26 | 27 | Here's an example how to create a Debian repo: 28 | 29 | ```starlark 30 | apt = use_extension("@rules_distroless//apt:extensions.bzl", "apt") 31 | apt.install( 32 | name = "bullseye", 33 | lock = "//examples/apt:bullseye.lock.json", 34 | manifest = "//examples/apt:bullseye.yaml", 35 | ) 36 | use_repo(apt, "bullseye") 37 | ``` 38 | 39 | Note that, for the initial setup (or if we want to run without a lock) the 40 | lockfile attribute can be omitted. All you need is a YAML 41 | [manifest](/examples/debian_snapshot/bullseye.yaml): 42 | ```yaml 43 | version: 1 44 | 45 | sources: 46 | - channel: bullseye main 47 | url: https://snapshot-cloudflare.debian.org/archive/debian/20240210T223313Z 48 | 49 | archs: 50 | - amd64 51 | 52 | packages: 53 | - perl 54 | ``` 55 | 56 | `apt.install` will parse the manifest and will fetch and install the packages 57 | for the given architectures in the Bazel repo `@`. 58 | 59 | Each `/` has two targets that match the usual structure of a 60 | Debian package: `data` and `control`. 61 | 62 | You can use the package like so: `@///:`. 63 | 64 | E.g. for the previous example, you could use `@bullseye//perl/amd64:data`. 65 | 66 | ### Lockfiles 67 | 68 | As mentioned, the macro can be used without a lock because the lock will be 69 | generated internally on-demand. However, this comes with the cost of 70 | performing a new package resolution on repository cache misses. 71 | 72 | The lockfile can be generated by running `bazel run @bullseye//:lock`. This 73 | will generate a `.lock.json` file of the same name and in the same path as 74 | the YAML `manifest` file. 75 | 76 | If you explicitly want to run without a lock and avoid the warning messages 77 | set the `nolock` argument to `True`. 78 | 79 | ### Best Practice: use snapshot archive URLs 80 | 81 | While we strongly encourage users to check in the generated lockfile, it's 82 | not always possible because Debian repositories are rolling by default. 83 | Therefore, a lockfile generated today might not work later if the upstream 84 | repository removes or publishes a new version of a package. 85 | 86 | To avoid this problems and increase the reproducibility it's recommended to 87 | avoid using normal Debian mirrors and use snapshot archives instead. 88 | 89 | Snapshot archives provide a way to access Debian package mirrors at a point 90 | in time. Basically, it's a "wayback machine" that allows access to (almost) 91 | all past and current packages based on dates and version numbers. 92 | 93 | Debian has had snapshot archives for [10+ 94 | years](https://lists.debian.org/debian-announce/2010/msg00002.html). Ubuntu 95 | began providing a similar service recently and has packages available since 96 | March 1st 2023. 97 | 98 | To use this services simply use a snapshot URL in the manifest. Here's two 99 | examples showing how to do this for Debian and Ubuntu: 100 | * [/examples/debian_snapshot](/examples/debian_snapshot) 101 | * [/examples/ubuntu_snapshot](/examples/ubuntu_snapshot) 102 | 103 | For more infomation, please check https://snapshot.debian.org and/or 104 | https://snapshot.ubuntu.com. 105 | 106 | **Attributes** 107 | 108 | | Name | Description | Type | Mandatory | Default | 109 | | :------------- | :------------- | :------------- | :------------- | :------------- | 110 | | lock | The lock file to use for the index. | Label | optional | `None` | 111 | | manifest | The file used to generate the lock file | Label | required | | 112 | | mergedusr | Whether packges should be normalized following mergedusr conventions. Turning this on might fix the following error thrown by docker for ambigious paths: `duplicate of paths are supported.` For more context please see https://salsa.debian.org/md/usrmerge/-/raw/master/debian/README.Debian?ref_type=heads | Boolean | optional | `False` | 113 | | nolock | If you explicitly want to run without a lock, set it to `True` to avoid the DEBUG messages. | Boolean | optional | `False` | 114 | | package_template | (EXPERIMENTAL!) a template file for generated BUILD files. | Label | optional | `None` | 115 | | resolve_transitive | Whether dependencies of dependencies should be resolved and added to the lockfile. | Boolean | optional | `True` | 116 | 117 | 118 | -------------------------------------------------------------------------------- /docs/apt_macro.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | `apt.install` macro 4 | 5 | This documentation provides an overview of the convenience `apt.install` 6 | repository macro to create Debian repositories with packages "installed" in 7 | them and available to use in Bazel. 8 | 9 | 10 | 11 | ## apt.install 12 | 13 |
 14 | load("@rules_distroless//apt:apt.bzl", "apt")
 15 | 
 16 | apt.install(name, manifest, lock, nolock, package_template, resolve_transitive)
 17 | 
18 | 19 | Repository macro to create Debian repositories. 20 | 21 | > [!WARNING] 22 | > THIS IS A LEGACY MACRO. Use it only if you are still using `WORKSPACE`. 23 | > Otherwise please use the [`apt` module extension](apt.md). 24 | 25 | Here's an example to create a Debian repo with `apt.install`: 26 | 27 | ```starlark 28 | # WORKSPACE 29 | 30 | load("@rules_distroless//apt:apt.bzl", "apt") 31 | 32 | apt.install( 33 | name = "bullseye", 34 | # lock = "//examples/apt:bullseye.lock.json", 35 | manifest = "//examples/apt:bullseye.yaml", 36 | ) 37 | 38 | load("@bullseye//:packages.bzl", "bullseye_packages") 39 | bullseye_packages() 40 | ``` 41 | 42 | Note that, for the initial setup (or if we want to run without a lock) the 43 | lockfile attribute can be omitted. All you need is a YAML 44 | [manifest](/examples/debian_snapshot/bullseye.yaml): 45 | ```yaml 46 | version: 1 47 | 48 | sources: 49 | - channel: bullseye main 50 | url: https://snapshot-cloudflare.debian.org/archive/debian/20240210T223313Z 51 | 52 | archs: 53 | - amd64 54 | 55 | packages: 56 | - perl 57 | ``` 58 | 59 | `apt.install` will parse the manifest and will fetch and install the 60 | packages for the given architectures in the Bazel repo `@`. 61 | 62 | Each `/` has two targets that match the usual structure of a 63 | Debian package: `data` and `control`. 64 | 65 | You can use the package like so: `@///:`. 66 | 67 | E.g. for the previous example, you could use `@bullseye//perl/amd64:data`. 68 | 69 | ### Lockfiles 70 | 71 | As mentioned, the macro can be used without a lock because the lock will be 72 | generated internally on-demand. However, this comes with the cost of 73 | performing a new package resolution on repository cache misses. 74 | 75 | The lockfile can be generated by running `bazel run @bullseye//:lock`. This 76 | will generate a `.lock.json` file of the same name and in the same path as 77 | the YAML `manifest` file. 78 | 79 | If you explicitly want to run without a lock and avoid the warning messages 80 | set the `nolock` argument to `True`. 81 | 82 | ### Best Practice: use snapshot archive URLs 83 | 84 | While we strongly encourage users to check in the generated lockfile, it's 85 | not always possible because Debian repositories are rolling by default. 86 | Therefore, a lockfile generated today might not work later if the upstream 87 | repository removes or publishes a new version of a package. 88 | 89 | To avoid this problems and increase the reproducibility it's recommended to 90 | avoid using normal Debian mirrors and use snapshot archives instead. 91 | 92 | Snapshot archives provide a way to access Debian package mirrors at a point 93 | in time. Basically, it's a "wayback machine" that allows access to (almost) 94 | all past and current packages based on dates and version numbers. 95 | 96 | Debian has had snapshot archives for [10+ 97 | years](https://lists.debian.org/debian-announce/2010/msg00002.html). Ubuntu 98 | began providing a similar service recently and has packages available since 99 | March 1st 2023. 100 | 101 | To use this services simply use a snapshot URL in the manifest. Here's two 102 | examples showing how to do this for Debian and Ubuntu: 103 | * [/examples/debian_snapshot](/examples/debian_snapshot) 104 | * [/examples/ubuntu_snapshot](/examples/ubuntu_snapshot) 105 | 106 | For more infomation, please check https://snapshot.debian.org and/or 107 | https://snapshot.ubuntu.com. 108 | 109 | 110 | **PARAMETERS** 111 | 112 | 113 | | Name | Description | Default Value | 114 | | :------------- | :------------- | :------------- | 115 | | name | name of the repository | none | 116 | | manifest | label to a `manifest.yaml` | none | 117 | | lock | label to a `lock.json` | `None` | 118 | | nolock | bool, set to True if you explicitly want to run without a lock and avoid the DEBUG messages. | `False` | 119 | | package_template | (EXPERIMENTAL!) a template file for generated BUILD files. Available template replacement keys are: `{target_name}`, `{deps}`, `{urls}`, `{name}`, `{arch}`, `{sha256}`, `{repo_name}` | `None` | 120 | | resolve_transitive | whether dependencies of dependencies should be resolved and added to the lockfile. | `True` | 121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/rules.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Public API re-exports 4 | 5 | 6 | 7 | ## cacerts 8 | 9 |
 10 | load("@rules_distroless//distroless:defs.bzl", "cacerts")
 11 | 
 12 | cacerts(name, mode, package, time)
 13 | 
14 | 15 | Create a ca-certificates.crt bundle from Common CA certificates. 16 | 17 | When provided with the `ca-certificates` Debian package it will create a bundle 18 | of all common CA certificates at `/usr/share/ca-certificates` and bundle them into 19 | a `ca-certificates.crt` file at `/etc/ssl/certs/ca-certificates.crt` 20 | 21 | An example of this would be 22 | 23 | ```starlark 24 | # MODULE.bazel 25 | http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 26 | 27 | http_archive( 28 | name = "ca-certificates", 29 | type = ".deb", 30 | sha256 = "b2d488ad4d8d8adb3ba319fc9cb2cf9909fc42cb82ad239a26c570a2e749c389", 31 | urls = ["https://snapshot.debian.org/archive/debian/20231106T210201Z/pool/main/c/ca-certificates/ca-certificates_20210119_all.deb"], 32 | build_file_content = "exports_files(["data.tar.xz"])" 33 | ) 34 | 35 | # BUILD.bazel 36 | load("@rules_distroless//distroless:defs.bzl", "cacerts") 37 | 38 | cacerts( 39 | name = "example", 40 | package = "@ca-certificates//:data.tar.xz", 41 | ) 42 | ``` 43 | 44 | To use the generated certificate bundle for SSL, **you must set SSL_CERT_FILE in the 45 | environment**. You can set it on the oci image like so: 46 | ```starlark 47 | oci_image( 48 | name = "my-image", 49 | env = { 50 | "SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt", 51 | } 52 | ) 53 | ``` 54 | 55 | **ATTRIBUTES** 56 | 57 | 58 | | Name | Description | Type | Mandatory | Default | 59 | | :------------- | :------------- | :------------- | :------------- | :------------- | 60 | | name | A unique name for this target. | Name | required | | 61 | | mode | mode for the entries | String | optional | `"0555"` | 62 | | package | - | Label | required | | 63 | | time | time for the entries | String | optional | `"0.0"` | 64 | 65 | 66 | 67 | 68 | ## flatten 69 | 70 |
 71 | load("@rules_distroless//distroless:defs.bzl", "flatten")
 72 | 
 73 | flatten(name, compress, deduplicate, tars)
 74 | 
75 | 76 | Flatten multiple archives into single archive. 77 | 78 | **ATTRIBUTES** 79 | 80 | 81 | | Name | Description | Type | Mandatory | Default | 82 | | :------------- | :------------- | :------------- | :------------- | :------------- | 83 | | name | A unique name for this target. | Name | required | | 84 | | compress | Compress the archive file with a supported algorithm. | String | optional | `""` | 85 | | deduplicate | EXPERIMENTAL: We may change or remove it without a notice.

Remove duplicate entries from the archives after flattening. Deduplication is performed only for directories.

This requires `awk` to be available in the PATH. | Boolean | optional | `False` | 86 | | tars | List of tars to flatten | List of labels | required | | 87 | 88 | 89 | 90 | 91 | ## java_keystore 92 | 93 |
 94 | load("@rules_distroless//distroless:defs.bzl", "java_keystore")
 95 | 
 96 | java_keystore(name, certificates, mode, time)
 97 | 
98 | 99 | Create a java keystore (database) of cryptographic keys, X.509 certificate chains, and trusted certificates. 100 | 101 | Currently only public X.509 are supported as part of the PUBLIC API contract. 102 | 103 | **ATTRIBUTES** 104 | 105 | 106 | | Name | Description | Type | Mandatory | Default | 107 | | :------------- | :------------- | :------------- | :------------- | :------------- | 108 | | name | A unique name for this target. | Name | required | | 109 | | certificates | - | List of labels | required | | 110 | | mode | mode for the entries | String | optional | `"0755"` | 111 | | time | time for the entries | String | optional | `"0.0"` | 112 | 113 | 114 | 115 | 116 | ## locale 117 | 118 |
119 | load("@rules_distroless//distroless:defs.bzl", "locale")
120 | 
121 | locale(name, charset, package, time)
122 | 
123 | 124 | Create a locale archive from a Debian package. 125 | 126 | An example of this would be 127 | 128 | ```starlark 129 | # MODULE.bazel 130 | http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 131 | 132 | http_archive( 133 | name = "libc-bin", 134 | build_file_content = 'exports_files(["data.tar.xz"])', 135 | sha256 = "8b048ab5c7e9f5b7444655541230e689631fd9855c384e8c4a802586d9bbc65a", 136 | urls = ["https://snapshot.debian.org/archive/debian-security/20231106T230332Z/pool/updates/main/g/glibc/libc-bin_2.31-13+deb11u7_amd64.deb"], 137 | ) 138 | 139 | # BUILD.bazel 140 | load("@rules_distroless//distroless:defs.bzl", "locale") 141 | 142 | locale( 143 | name = "example", 144 | package = "@libc-bin//:data.tar.xz" 145 | ) 146 | ``` 147 | 148 | **ATTRIBUTES** 149 | 150 | 151 | | Name | Description | Type | Mandatory | Default | 152 | | :------------- | :------------- | :------------- | :------------- | :------------- | 153 | | name | A unique name for this target. | Name | required | | 154 | | charset | - | String | optional | `"C.utf8"` | 155 | | package | - | Label | required | | 156 | | time | time for the entries | String | optional | `"0.0"` | 157 | 158 | 159 | 160 | 161 | ## group 162 | 163 |
164 | load("@rules_distroless//distroless:defs.bzl", "group")
165 | 
166 | group(name, entries, time, mode, **kwargs)
167 | 
168 | 169 | Create a group file from array of dicts. 170 | 171 | https://www.ibm.com/docs/en/aix/7.2?topic=files-etcgroup-file#group_security__a21597b8__title__1 172 | 173 | 174 | **PARAMETERS** 175 | 176 | 177 | | Name | Description | Default Value | 178 | | :------------- | :------------- | :------------- | 179 | | name | name of the target | none | 180 | | entries | an array of dicts which will be serialized into single group file. | none | 181 | | time | time for the entry | `"0.0"` | 182 | | mode | mode for the entry | `"0644"` | 183 | | kwargs | other named arguments to expanded targets. see [common rule attributes](https://bazel.build/reference/be/common-definitions#common-attributes). | none | 184 | 185 | 186 | 187 | 188 | ## home 189 | 190 |
191 | load("@rules_distroless//distroless:defs.bzl", "home")
192 | 
193 | home(name, dirs, **kwargs)
194 | 
195 | 196 | Create home directories with specific uid and gids. 197 | 198 | **PARAMETERS** 199 | 200 | 201 | | Name | Description | Default Value | 202 | | :------------- | :------------- | :------------- | 203 | | name | name of the target | none | 204 | | dirs | array of home directory dicts. | none | 205 | | kwargs | other named arguments to that is passed to tar. see [common rule attributes](https://bazel.build/reference/be/common-definitions#common-attributes). | none | 206 | 207 | 208 | 209 | 210 | ## os_release 211 | 212 |
213 | load("@rules_distroless//distroless:defs.bzl", "os_release")
214 | 
215 | os_release(name, content, path, mode, time, **kwargs)
216 | 
217 | 218 | Create an Operating System Identification file from a key, value dictionary. 219 | 220 | https://www.freedesktop.org/software/systemd/man/latest/os-release.html 221 | 222 | 223 | **PARAMETERS** 224 | 225 | 226 | | Name | Description | Default Value | 227 | | :------------- | :------------- | :------------- | 228 | | name | name of the target | none | 229 | | content | a key, value dictionary that will be serialized into `=` seperated lines.

See https://www.freedesktop.org/software/systemd/man/latest/os-release.html#Options for well known keys. | none | 230 | | path | where to put the file in the result archive. default: `/usr/lib/os-release` | `"/usr/lib/os-release"` | 231 | | mode | mode for the entry | `"0555"` | 232 | | time | time for the entry | `"0"` | 233 | | kwargs | other named arguments to expanded targets. see [common rule attributes](https://bazel.build/reference/be/common-definitions#common-attributes). | none | 234 | 235 | 236 | 237 | 238 | ## passwd 239 | 240 |
241 | load("@rules_distroless//distroless:defs.bzl", "passwd")
242 | 
243 | passwd(name, entries, mode, time, **kwargs)
244 | 
245 | 246 | Create a passwd file from array of dicts. 247 | 248 | https://www.ibm.com/docs/en/aix/7.3?topic=passwords-using-etcpasswd-file 249 | 250 | 251 | **PARAMETERS** 252 | 253 | 254 | | Name | Description | Default Value | 255 | | :------------- | :------------- | :------------- | 256 | | name | name of the target | none | 257 | | entries | an array of dicts which will be serialized into single passwd file.

An example;

dict(gid = 0, uid = 0, home = "/root", shell = "/bin/bash", username = "root")
| none | 258 | | mode | mode for the entry | `"0644"` | 259 | | time | time for the entry | `"0.0"` | 260 | | kwargs | other named arguments to expanded targets. see [common rule attributes](https://bazel.build/reference/be/common-definitions#common-attributes). | none | 261 | 262 | 263 | -------------------------------------------------------------------------------- /e2e/smoke/.bazelversion: -------------------------------------------------------------------------------- 1 | ../../.bazelversion -------------------------------------------------------------------------------- /e2e/smoke/BUILD: -------------------------------------------------------------------------------- 1 | """ 2 | NOTE: 3 | 4 | This is the main test used in the e2e testing. 5 | 6 | PLEASE KEEP e2e/smoke/BUILD and examples/debian_snapshot/BUILD 7 | IN-SYNC WITH EACH OTHER, AS WELL AS THE REST OF THE TEST FILES 8 | (test_linux_ files and the bullseye YAML manifest) 9 | """ 10 | 11 | load("@aspect_bazel_lib//lib:tar.bzl", "tar") 12 | load("@aspect_bazel_lib//lib:transitions.bzl", "platform_transition_filegroup") 13 | load("@container_structure_test//:defs.bzl", "container_structure_test") 14 | load("@rules_distroless//distroless:defs.bzl", "cacerts", "group", "passwd") 15 | load("@rules_oci//oci:defs.bzl", "oci_image", "oci_load") 16 | 17 | COMPATIBLE_WITH = select({ 18 | "@platforms//cpu:x86_64": ["@platforms//cpu:x86_64"], 19 | "@platforms//cpu:arm64": ["@platforms//cpu:arm64"], 20 | }) + [ 21 | "@platforms//os:linux", 22 | ] 23 | 24 | passwd( 25 | name = "passwd", 26 | entries = [ 27 | { 28 | "uid": 0, 29 | "gid": 0, 30 | "home": "/root", 31 | "shell": "/bin/bash", 32 | "username": "r00t", 33 | }, 34 | { 35 | "uid": 100, 36 | "gid": 65534, 37 | "home": "/home/_apt", 38 | "shell": "/usr/sbin/nologin", 39 | "username": "_apt", 40 | }, 41 | ], 42 | ) 43 | 44 | group( 45 | name = "group", 46 | entries = [ 47 | { 48 | "name": "root", 49 | "gid": 0, 50 | }, 51 | { 52 | "name": "_apt", 53 | "gid": 65534, 54 | }, 55 | ], 56 | ) 57 | 58 | tar( 59 | name = "sh", 60 | mtree = [ 61 | # needed as dpkg assumes sh is installed in a typical debian installation. 62 | "./bin/sh type=link link=/bin/bash", 63 | ], 64 | ) 65 | 66 | cacerts( 67 | name = "cacerts", 68 | package = "@bullseye//ca-certificates:data", 69 | target_compatible_with = COMPATIBLE_WITH, 70 | ) 71 | 72 | oci_image( 73 | name = "image", 74 | architecture = select({ 75 | "@platforms//cpu:arm64": "arm64", 76 | "@platforms//cpu:x86_64": "amd64", 77 | }), 78 | env = { 79 | # Required to use the SSL certs from `cacerts()` 80 | "SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt", 81 | }, 82 | os = "linux", 83 | # NOTE: this is needed because, otherwise, bazel test //... fails, even 84 | # when container_structure_test already has target_compatible_with. 85 | # See 136 86 | target_compatible_with = COMPATIBLE_WITH, 87 | tars = [ 88 | # This target contains all the installed packages. 89 | "@bullseye//:flat", 90 | ":sh", 91 | ":passwd", 92 | ":group", 93 | ":cacerts", 94 | ], 95 | ) 96 | 97 | platform( 98 | name = "linux_arm64", 99 | constraint_values = [ 100 | "@platforms//os:linux", 101 | "@platforms//cpu:arm64", 102 | ], 103 | ) 104 | 105 | platform( 106 | name = "linux_amd64", 107 | constraint_values = [ 108 | "@platforms//os:linux", 109 | "@platforms//cpu:x86_64", 110 | ], 111 | ) 112 | 113 | platform_transition_filegroup( 114 | name = "image_platform", 115 | srcs = [":image"], 116 | target_platform = select({ 117 | "@platforms//cpu:arm64": ":linux_arm64", 118 | "@platforms//cpu:x86_64": ":linux_amd64", 119 | }), 120 | ) 121 | 122 | oci_load( 123 | name = "tarball", 124 | image = ":image_platform", 125 | repo_tags = [ 126 | "distroless/test:latest", 127 | ], 128 | # NOTE: this is needed because, otherwise, bazel test //... fails, even 129 | # when container_structure_test already has target_compatible_with. 130 | # See 136 131 | target_compatible_with = COMPATIBLE_WITH, 132 | ) 133 | 134 | container_structure_test( 135 | name = "test", 136 | configs = select({ 137 | "@platforms//cpu:arm64": ["test_linux_arm64.yaml"], 138 | "@platforms//cpu:x86_64": ["test_linux_amd64.yaml"], 139 | }), 140 | image = ":image_platform", 141 | target_compatible_with = COMPATIBLE_WITH, 142 | ) 143 | -------------------------------------------------------------------------------- /e2e/smoke/MODULE.bazel: -------------------------------------------------------------------------------- 1 | bazel_dep(name = "rules_distroless", version = "0.0.0", dev_dependency = True) 2 | bazel_dep(name = "bazel_skylib", version = "1.5.0", dev_dependency = True) 3 | bazel_dep(name = "platforms", version = "0.0.10", dev_dependency = True) 4 | bazel_dep(name = "rules_oci", version = "2.0.0", dev_dependency = True) 5 | bazel_dep(name = "container_structure_test", version = "1.16.0", dev_dependency = True) 6 | bazel_dep(name = "aspect_bazel_lib", version = "2.7.3", dev_dependency = True) 7 | 8 | local_path_override( 9 | module_name = "rules_distroless", 10 | path = "../..", 11 | ) 12 | 13 | apt = use_extension("@rules_distroless//apt:extensions.bzl", "apt") 14 | apt.install( 15 | name = "bullseye", 16 | lock = ":bullseye.lock.json", 17 | manifest = ":bullseye.yaml", 18 | ) 19 | apt.install( 20 | name = "bullseye_nolock", 21 | manifest = ":bullseye.yaml", 22 | nolock = True, 23 | ) 24 | 25 | # bazel run @bullseye//:lock 26 | use_repo(apt, "bullseye", "bullseye_nolock") 27 | -------------------------------------------------------------------------------- /e2e/smoke/README.md: -------------------------------------------------------------------------------- 1 | # smoke test 2 | 3 | This e2e exercises the repo from an end-users perpective. 4 | It catches mistakes in our install instructions, or usages that fail when called from an "external" repository to rules_distroless. 5 | It is also used by the presubmit check for the Bazel Central Registry. 6 | -------------------------------------------------------------------------------- /e2e/smoke/bullseye.yaml: -------------------------------------------------------------------------------- 1 | # Packages for examples/debian_snapshot. 2 | # 3 | # Anytime this file is changed, the lockfile needs to be regenerated. 4 | # 5 | # To generate the bullseye.lock.json run the following command 6 | # 7 | # bazel run @bullseye//:lock 8 | # 9 | # See debian_package_index at WORKSPACE.bazel 10 | version: 1 11 | 12 | sources: 13 | - channel: bullseye main contrib 14 | urls: 15 | - https://snapshot-cloudflare.debian.org/archive/debian/20240210T223313Z 16 | - https://snapshot.debian.org/archive/debian/20240210T223313Z 17 | - channel: bullseye-security main 18 | url: https://snapshot-cloudflare.debian.org/archive/debian-security/20240210T223313Z 19 | - channel: bullseye-updates main 20 | url: https://snapshot-cloudflare.debian.org/archive/debian/20240210T223313Z/ 21 | - channel: cloud-sdk main 22 | url: https://packages.cloud.google.com/apt 23 | 24 | archs: 25 | - "amd64" 26 | - "arm64" 27 | 28 | packages: 29 | - "ncurses-base" 30 | - "libncurses6" 31 | - "tzdata" 32 | - "coreutils" 33 | - "dpkg" 34 | - "apt" 35 | - "perl" 36 | - "ca-certificates" 37 | - "nvidia-kernel-common" 38 | - "bash" 39 | - "nginx-full" 40 | - "nginx-core" 41 | - "google-cloud-cli" 42 | -------------------------------------------------------------------------------- /e2e/smoke/test_linux_amd64.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | 3 | commandTests: 4 | - name: "echo hello" 5 | command: "/bin/bash" 6 | args: ["-c", "echo hello world!"] 7 | expectedOutput: ["hello world!"] 8 | - name: "apt list --installed" 9 | command: "apt" 10 | args: ["list", "--installed"] 11 | expectedOutput: 12 | - Listing\.\.\. 13 | - apt/now 2\.2\.4 amd64 \[installed,local\] 14 | - bash/now 5\.1-2\+deb11u1 amd64 \[installed,local\] 15 | - coreutils/now 8\.32-4\+b1 amd64 \[installed,local\] 16 | - dpkg/now 1\.20\.13 amd64 \[installed,local\] 17 | - libncurses6/now 6\.2\+20201114-2\+deb11u2 amd64 \[installed,local\] 18 | - ncurses-base/now 6\.2\+20201114-2\+deb11u2 all \[installed,local\] 19 | - perl/now 5\.32\.1-4\+deb11u3 amd64 \[installed,local\] 20 | - tzdata/now 2024a-0\+deb11u1 all \[installed,local\] 21 | - nvidia-kernel-common/now 20151021\+13 amd64 \[installed,local\] 22 | - name: "whoami" 23 | command: "whoami" 24 | expectedOutput: [r00t] 25 | - name: "naive ca-certs check" 26 | command: "head" 27 | args: ["-1", "/etc/ssl/certs/ca-certificates.crt"] 28 | expectedOutput: [-----BEGIN CERTIFICATE-----] 29 | - name: "in depth ca-certs check" 30 | command: "/usr/bin/openssl" 31 | args: ["s_client", "-connect", "www.google.com:443"] 32 | expectedOutput: ["Verify return code: 0 .ok."] 33 | -------------------------------------------------------------------------------- /e2e/smoke/test_linux_arm64.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | 3 | commandTests: 4 | - name: "echo hello" 5 | command: "/bin/bash" 6 | args: ["-c", "echo hello world!"] 7 | expectedOutput: ["hello world!"] 8 | - name: "apt list --installed" 9 | command: "apt" 10 | args: ["list", "--installed"] 11 | expectedOutput: 12 | - Listing\.\.\. 13 | - apt/now 2\.2\.4 arm64 \[installed,local\] 14 | - bash/now 5\.1-2\+deb11u1 arm64 \[installed,local\] 15 | - coreutils/now 8\.32-4 arm64 \[installed,local\] 16 | - dpkg/now 1\.20\.13 arm64 \[installed,local\] 17 | - libncurses6/now 6\.2\+20201114-2\+deb11u2 arm64 \[installed,local\] 18 | - ncurses-base/now 6\.2\+20201114-2\+deb11u2 all \[installed,local\] 19 | - perl/now 5\.32\.1-4\+deb11u3 arm64 \[installed,local\] 20 | - tzdata/now 2024a-0\+deb11u1 all \[installed,local\] 21 | - nvidia-kernel-common/now 20151021\+13 arm64 \[installed,local\] 22 | - name: "whoami" 23 | command: "whoami" 24 | expectedOutput: [r00t] 25 | - name: "naive ca-certs check" 26 | command: "head" 27 | args: ["-1", "/etc/ssl/certs/ca-certificates.crt"] 28 | expectedOutput: [-----BEGIN CERTIFICATE-----] 29 | - name: "in depth ca-certs check" 30 | command: "/usr/bin/openssl" 31 | args: ["s_client", "-connect", "www.google.com:443"] 32 | expectedOutput: ["Verify return code: 0 .ok."] 33 | --------------------------------------------------------------------------------