├── .node-version ├── test ├── latest │ └── src │ │ ├── test │ │ ├── ca.srl │ │ ├── request.php │ │ ├── request.py │ │ ├── request.rb │ │ ├── request.mjs │ │ ├── cert.conf │ │ └── ca.conf │ │ └── etc │ │ └── nginx │ │ └── sites-enabled │ │ └── default ├── ruby │ └── test │ │ └── b │ │ ├── CPDAcknowledgements │ │ └── private │ │ │ └── .gitkeep │ │ ├── Project │ │ ├── Podfile │ │ └── Podfile.lock │ │ └── CPDAcknowledgements.podspec ├── dart │ ├── test │ │ ├── .gitignore │ │ └── a │ │ │ └── pubspec.yaml │ └── Dockerfile.arm64 ├── flutter │ ├── test │ │ ├── .gitignore │ │ ├── d │ │ │ └── pubspec.yaml │ │ └── a │ │ │ ├── pubspec.yaml │ │ │ └── pubspec.lock │ └── Dockerfile.arm64 ├── types.d.ts ├── node │ └── test │ │ ├── a │ │ ├── .yarnrc.yml │ │ └── package.json │ │ └── b │ │ ├── package.json │ │ └── yarn.lock ├── rust │ ├── test │ │ └── a │ │ │ ├── src │ │ │ └── main.rs │ │ │ ├── crate1 │ │ │ ├── src │ │ │ │ └── main.rs │ │ │ └── Cargo.toml │ │ │ ├── crate2 │ │ │ ├── src │ │ │ │ └── main.rs │ │ │ └── Cargo.toml │ │ │ ├── crate5 │ │ │ ├── src │ │ │ │ └── main.rs │ │ │ └── Cargo.toml │ │ │ ├── subdir │ │ │ ├── crate3 │ │ │ │ ├── src │ │ │ │ │ └── main.rs │ │ │ │ └── Cargo.toml │ │ │ └── crate4 │ │ │ │ ├── src │ │ │ │ └── main.rs │ │ │ │ └── Cargo.toml │ │ │ └── Cargo.toml │ └── Dockerfile.arm64 ├── golang │ ├── test │ │ ├── d │ │ │ ├── vendor │ │ │ │ ├── modules.txt │ │ │ │ └── github.com │ │ │ │ │ └── pkg │ │ │ │ │ └── errors │ │ │ │ │ ├── .travis.yml │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── appveyor.yml │ │ │ │ │ └── LICENSE │ │ │ ├── go.sum │ │ │ └── go.mod │ │ ├── c │ │ │ └── go.mod │ │ ├── b │ │ │ ├── go.mod │ │ │ └── go.sum │ │ └── a │ │ │ ├── go.mod │ │ │ └── go.sum │ └── Dockerfile.arm64 ├── dotnet │ ├── test │ │ ├── Class1.cs │ │ ├── test.csproj │ │ └── packages.lock.json │ └── Dockerfile.arm64 ├── python │ └── test │ │ ├── f │ │ └── requirements.txt │ │ ├── b-conan │ │ └── conanfile.txt │ │ ├── a │ │ └── Pipfile │ │ ├── pipenv-b │ │ └── Pipfile │ │ ├── d-poetry │ │ └── pyproject.toml │ │ └── c-poetry │ │ └── pyproject.toml ├── java │ └── test │ │ └── sbt │ │ ├── src │ │ └── main │ │ │ └── scala │ │ │ └── example │ │ │ └── Hello.scala │ │ └── build.sbt ├── php │ ├── test │ │ └── a │ │ │ └── composer.json │ └── Dockerfile.arm64 ├── jb │ ├── test │ │ ├── jsonnetfile.json │ │ └── jsonnetfile.lock.json │ └── Dockerfile.arm64 ├── swift │ ├── test │ │ ├── a │ │ │ ├── Package.resolved │ │ │ └── Package.swift │ │ ├── b │ │ │ ├── Package.resolved │ │ │ └── Package.swift │ │ └── c │ │ │ ├── Package.resolved │ │ │ └── Package.swift │ └── Dockerfile.arm64 ├── di.ts ├── nix │ ├── test │ │ ├── flake.lock │ │ └── flake.nix │ └── Dockerfile.arm64 ├── mock.ts ├── path.ts ├── bash │ ├── cache.sh │ ├── util.sh │ ├── v2 │ │ └── defaults.bats │ └── linking.bats ├── flux │ └── Dockerfile.arm64 ├── helm │ └── Dockerfile.arm64 ├── powershell │ ├── Dockerfile.arm64 │ └── Dockerfile ├── global-setup.ts ├── erlang │ └── Dockerfile.arm64 └── http-mock.ts ├── .npmrc ├── .husky └── pre-commit ├── .dockerignore ├── tools ├── containerbase.acl ├── prepare-proxy.js ├── utils.js ├── bats.js └── prepare-release.js ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── codecov.yml ├── tsconfig.dist.json ├── src ├── cli │ ├── index.ts │ ├── tools │ │ ├── ruby │ │ │ ├── schema.ts │ │ │ ├── index.ts │ │ │ └── cocoapods.ts │ │ ├── nix.ts │ │ ├── jb.ts │ │ ├── vendir.ts │ │ ├── terraform.ts │ │ ├── git │ │ │ └── lfs.ts │ │ ├── java │ │ │ ├── scala.ts │ │ │ ├── sbt.ts │ │ │ ├── schema.ts │ │ │ └── resolver.ts │ │ ├── node │ │ │ └── schema.ts │ │ ├── rust.ts │ │ ├── swift.ts │ │ ├── golang.ts │ │ ├── __mocks__ │ │ │ └── bun.ts │ │ ├── erlang │ │ │ ├── index.ts │ │ │ └── elixir.ts │ │ ├── python │ │ │ ├── index.ts │ │ │ ├── pip.ts │ │ │ ├── schema.ts │ │ │ └── poetry.ts │ │ ├── powershell.ts │ │ ├── php │ │ │ └── __mocks__ │ │ │ │ └── composer.ts │ │ ├── protoc.ts │ │ ├── bazelisk.ts │ │ ├── devbox.ts │ │ ├── skopeo.ts │ │ ├── index.ts │ │ ├── sops.ts │ │ ├── flux.ts │ │ ├── kustomize.ts │ │ ├── pixi.ts │ │ ├── kubectl.ts │ │ ├── bun.ts │ │ └── helm.ts │ ├── types.d.ts │ ├── index.spec.ts │ ├── command │ │ ├── index.spec.ts │ │ ├── index.ts │ │ ├── utils.spec.ts │ │ ├── init-tool.spec.ts │ │ ├── cleanup-path.spec.ts │ │ ├── uninstall-gem.ts │ │ ├── uninstall-npm.ts │ │ ├── uninstall-pip.ts │ │ ├── utils.ts │ │ ├── uninstall-gem.spec.ts │ │ ├── uninstall-pip.spec.ts │ │ ├── uninstall-npm.spec.ts │ │ ├── prepare-tool.spec.ts │ │ ├── install-npm.ts │ │ ├── install-gem.ts │ │ ├── file-exists.spec.ts │ │ ├── install-pip.ts │ │ ├── cleanup-path.ts │ │ ├── uninstall-tool.spec.ts │ │ ├── init-tool.ts │ │ └── link-tool.spec.ts │ ├── utils │ │ ├── versions.spec.ts │ │ ├── v2-tool.spec.ts │ │ ├── codes.ts │ │ ├── v2-tool.ts │ │ ├── types.ts │ │ ├── hash.ts │ │ ├── versions.ts │ │ ├── hash.spec.ts │ │ ├── index.ts │ │ ├── index.spec.ts │ │ └── logger.ts │ ├── install-tool │ │ ├── tool-version-resolver.ts │ │ └── tool-version-resolver.service.ts │ ├── main.spec.ts │ ├── proxy.ts │ ├── services │ │ ├── compression.service.spec.ts │ │ ├── compression.service.ts │ │ ├── link-tool.service.spec.ts │ │ ├── index.ts │ │ ├── apt.service.spec.ts │ │ ├── apt.service.ts │ │ └── data.service.spec.ts │ ├── main.ts │ └── prepare-tool │ │ ├── base-prepare.service.ts │ │ ├── prepare-legacy-tools.service.ts │ │ └── index.spec.ts └── usr │ └── local │ ├── sbin │ └── install-containerbase │ └── containerbase │ ├── bin │ ├── cleanup-cache.sh │ ├── install-apt.sh │ ├── v1-install-tool.sh │ ├── docker-entrypoint.sh │ └── v2-install-tool.sh │ ├── utils │ ├── user.sh │ ├── v2 │ │ ├── overrides.sh │ │ ├── filesystem.sh │ │ └── defaults.sh │ ├── linking.sh │ ├── version.sh │ ├── constants.sh │ └── ruby.sh │ └── tools │ ├── v2 │ ├── vendir.sh │ ├── scala.sh │ ├── terraform.sh │ ├── jb.sh │ ├── git-lfs.sh │ ├── powershell.sh │ ├── nix.sh │ └── sbt.sh │ └── git.sh ├── .prettierrc.json ├── .gitignore ├── tsconfig.lint.json ├── .prettierignore ├── pnpm-workspace.yaml ├── .editorconfig ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── actions │ ├── prepare-proxy │ │ └── action.yml │ └── check │ │ └── action.yml └── workflows │ ├── build-pr.yml │ ├── devcontainer.yml │ ├── trivy.yml │ └── cancel-stale-merge-queue-workflows.yml ├── __mocks__ └── pino.ts ├── .lintstagedrc.json ├── patches ├── @semantic-release__github.patch ├── clipanion@3.2.1.patch ├── nano-spawn.patch └── global-agent.patch ├── tsconfig.json ├── vitest.config.ts ├── .markdownlint-cli2.jsonc ├── Dockerfile ├── LICENSE └── docs └── cdn.md /.node-version: -------------------------------------------------------------------------------- 1 | 24.12.0 2 | -------------------------------------------------------------------------------- /test/latest/src/test/ca.srl: -------------------------------------------------------------------------------- 1 | 1234 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | save-prefix = 3 | -------------------------------------------------------------------------------- /test/ruby/test/b/CPDAcknowledgements/private/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | lint-staged 4 | -------------------------------------------------------------------------------- /test/dart/test/.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .packages 3 | -------------------------------------------------------------------------------- /test/flutter/test/.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .packages 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !test 3 | !dist/docker 4 | !dist/cli 5 | -------------------------------------------------------------------------------- /test/types.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | declare var cacheDir: string; 3 | -------------------------------------------------------------------------------- /tools/containerbase.acl: -------------------------------------------------------------------------------- 1 | ppa.launchpad.net 2 | binaries.erlang-solutions.com 3 | -------------------------------------------------------------------------------- /test/node/test/a/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | enableInlineBuilds: true 3 | -------------------------------------------------------------------------------- /test/rust/test/a/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /test/rust/test/a/crate1/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /test/rust/test/a/crate2/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /test/rust/test/a/crate5/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /test/golang/test/d/vendor/modules.txt: -------------------------------------------------------------------------------- 1 | # github.com/pkg/errors v0.8.0 2 | github.com/pkg/errors 3 | -------------------------------------------------------------------------------- /test/rust/test/a/subdir/crate3/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /test/rust/test/a/subdir/crate4/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /test/latest/src/test/request.php: -------------------------------------------------------------------------------- 1 | 3.3.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/golang/test/d/vendor/github.com/pkg/errors/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go_import_path: github.com/pkg/errors 3 | go: 4 | - 1.4.3 5 | - 1.5.4 6 | - 1.6.2 7 | - 1.7.1 8 | - tip 9 | 10 | script: 11 | - go test -v ./... 12 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/containerbase/devcontainer:13.25.18 2 | 3 | USER root 4 | 5 | # install required packages for testing 6 | RUN set -x; \ 7 | install-apt \ 8 | lsb-release \ 9 | ; 10 | 11 | USER $USER_NAME 12 | -------------------------------------------------------------------------------- /test/python/test/a/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | numpy = ">=1.18.0" 8 | 9 | [dev-packages] 10 | flake8 = "*" 11 | twine = "*" 12 | bleach = "==3.1.1" 13 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/bin/install-apt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # shellcheck source=/dev/null 6 | . /usr/local/containerbase/util.sh 7 | 8 | require_root 9 | 10 | apt_install "$@" 11 | 12 | # cleanup 13 | rm -rf /var/lib/apt/lists/* /var/log/dpkg.* /var/log/apt 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "omnisharp.autoStart": false, 3 | "typescript.preferences.importModuleSpecifier": "project-relative", 4 | "search.exclude": { 5 | "**/.yarn": true, 6 | "**/.pnp.*": true 7 | }, 8 | "files.associations": { 9 | "*.bats": "shellscript" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/golang/test/a/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/containerbase/test-golang1 2 | 3 | require github.com/pkg/errors v0.9.1 4 | require github.com/aws/aws-sdk-go v1.15.21 5 | require github.com/davecgh/go-spew v1.0.0 6 | 7 | replace github.com/gocql/gocql => github.com/kiwicom/gocql v0.0.0-20190701110745-b0d035b46104 8 | -------------------------------------------------------------------------------- /test/golang/test/d/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/containerbase/test-golang1 2 | 3 | require github.com/pkg/errors v0.7.0 4 | require github.com/aws/aws-sdk-go v1.15.21 5 | require github.com/davecgh/go-spew v1.0.0 6 | 7 | replace github.com/gocql/gocql => github.com/kiwicom/gocql v0.0.0-20190701110745-b0d035b46104 8 | -------------------------------------------------------------------------------- /test/latest/src/test/request.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | 3 | uri = URI('https://localhost') 4 | 5 | Net::HTTP.start(uri.host, uri.port, 6 | :use_ssl => uri.scheme == 'https') do |http| 7 | request = Net::HTTP::Get.new uri 8 | 9 | response = http.request request # Net::HTTPResponse object 10 | end 11 | -------------------------------------------------------------------------------- /.github/actions/prepare-proxy/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Prepare propxy' 2 | description: 'Prepares the apt proxy for the build' 3 | 4 | runs: 5 | using: 'composite' 6 | 7 | steps: 8 | - name: ⚙️ Prepare proxy 9 | shell: bash 10 | run: | 11 | sudo $(command -v node) tools/prepare-proxy.js 12 | -------------------------------------------------------------------------------- /test/python/test/pipenv-b/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | numpy = ">=1.18.0" 8 | psycopg2 = "*" 9 | 10 | [dev-packages] 11 | flake8 = "*" 12 | twine = "*" 13 | bleach = "==3.1.1" 14 | 15 | [requires] 16 | python_version = "3.10" 17 | -------------------------------------------------------------------------------- /test/dart/test/a/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: vector_victor 2 | description: A sample command-line application. 3 | version: 1.0.0 4 | # homepage: https://www.example.com 5 | 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | 9 | dependencies: 10 | path: ^1.8.0 11 | 12 | dev_dependencies: 13 | lints: ^1.0.1 14 | test: ^1.16.0 15 | -------------------------------------------------------------------------------- /test/dotnet/test/test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netcoreapp3.1;net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /__mocks__/pino.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | export const levels = { values: { info: 30 } }; 4 | export const pino = vi.fn().mockReturnValue({ 5 | trace: vi.fn(), 6 | debug: vi.fn(), 7 | info: vi.fn(), 8 | warn: vi.fn(), 9 | error: vi.fn(), 10 | fatal: vi.fn(), 11 | }); 12 | export const transport = vi.fn((a) => a); 13 | -------------------------------------------------------------------------------- /test/latest/src/test/request.mjs: -------------------------------------------------------------------------------- 1 | import https from 'https'; 2 | 3 | const options = { 4 | hostname: 'localhost', 5 | port: 443, 6 | path: '/', 7 | method: 'GET', 8 | }; 9 | 10 | const req = https.request(options); 11 | 12 | req.on('error', (error) => { 13 | console.error(error); 14 | process.exit(1); 15 | }); 16 | 17 | req.end(); 18 | -------------------------------------------------------------------------------- /.github/actions/check/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Check system' 2 | description: 'checks system status' 3 | 4 | runs: 5 | using: 'composite' 6 | 7 | steps: 8 | - name: ⚙️ Check docker service 9 | shell: bash 10 | run: | 11 | systemctl status docker 12 | - name: ⚙️ Check docker info 13 | shell: bash 14 | run: | 15 | docker info 16 | -------------------------------------------------------------------------------- /src/cli/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest'; 2 | 3 | const mocks = vi.hoisted(() => ({ 4 | main: vi.fn(), 5 | })); 6 | 7 | vi.mock('./main', () => mocks); 8 | 9 | describe('cli/index', () => { 10 | test('works', async () => { 11 | await import('./index'); 12 | expect(mocks.main).toHaveBeenCalledTimes(1); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/rust/test/a/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "workspace_example" 3 | version = "0.1.0" 4 | authors = [] 5 | 6 | [workspace] 7 | members = [ 8 | "crate1", 9 | "crate2", 10 | "subdir/crate3" 11 | ] 12 | 13 | [dependencies] 14 | crate4 = { path = "subdir/crate4", version = "0.1.0" } 15 | crate5 = { path = "crate5", version = "0.1.0" } 16 | serde = "1.0.106" 17 | -------------------------------------------------------------------------------- /test/jb/test/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/prometheus-operator/prometheus-operator.git", 8 | "subdir": "jsonnet/prometheus-operator" 9 | } 10 | }, 11 | "version": "v0.50.0" 12 | } 13 | ], 14 | "legacyImports": true 15 | } 16 | -------------------------------------------------------------------------------- /src/cli/tools/nix.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../install-tool/install-legacy-tool.service'; 3 | import { v2Tool } from '../utils/v2-tool'; 4 | 5 | @injectable() 6 | @injectFromHierarchy() 7 | @v2Tool('nix') 8 | export class NixInstallService extends V2ToolInstallService { 9 | override readonly name = 'nix'; 10 | } 11 | -------------------------------------------------------------------------------- /src/cli/tools/jb.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../install-tool/install-legacy-tool.service'; 3 | import { v2Tool } from '../utils/v2-tool'; 4 | 5 | @injectable() 6 | @injectFromHierarchy() 7 | @v2Tool('jb') 8 | export class JsonnetBundlerInstallService extends V2ToolInstallService { 9 | override readonly name = 'jb'; 10 | } 11 | -------------------------------------------------------------------------------- /src/cli/tools/vendir.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../install-tool/install-legacy-tool.service'; 3 | import { v2Tool } from '../utils/v2-tool'; 4 | 5 | @injectable() 6 | @injectFromHierarchy() 7 | @v2Tool('vendir') 8 | export class VendirInstallService extends V2ToolInstallService { 9 | override readonly name = 'vendir'; 10 | } 11 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.sh": "shellcheck", 3 | "*.bats": "shellcheck", 4 | ".husky/*": "shellcheck", 5 | "*.md": ["markdownlint-cli2 --fix", "prettier --write"], 6 | "src/usr/local/bin/*": "shellcheck", 7 | "src/usr/local/containerbase/bin/*": "shellcheck", 8 | "*.{js,ts,cjs,mjs}": ["eslint --fix", "prettier --write"], 9 | "!*.{cjs,js,mjs,md,ts}": "prettier --ignore-unknown --write" 10 | } 11 | -------------------------------------------------------------------------------- /tools/prepare-proxy.js: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | 3 | shell.config.fatal = true; 4 | 5 | shell.echo(`Preparing squid-deb-proxy`); 6 | 7 | shell.exec('apt-get -qq update'); 8 | shell.exec('apt-get install -y squid-deb-proxy'); 9 | shell 10 | .cat('./tools/containerbase.acl') 11 | .to('/etc/squid-deb-proxy/mirror-dstdomain.acl.d/containerbase.acl'); 12 | shell.exec('systemctl reload squid-deb-proxy'); 13 | -------------------------------------------------------------------------------- /src/cli/tools/terraform.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../install-tool/install-legacy-tool.service'; 3 | import { v2Tool } from '../utils/v2-tool'; 4 | 5 | @injectable() 6 | @injectFromHierarchy() 7 | @v2Tool('terraform') 8 | export class TerraformInstallService extends V2ToolInstallService { 9 | override readonly name = 'terraform'; 10 | } 11 | -------------------------------------------------------------------------------- /test/golang/test/d/vendor/github.com/pkg/errors/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/utils/user.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function createUser() { 4 | # Set up user and home directory with access to users in the root group (0) 5 | # https://docs.openshift.com/container-platform/3.6/creating_images/guidelines.html#use-uid 6 | groupadd --gid "${USER_ID}" "${USER_NAME}"; 7 | useradd --uid "${USER_ID}" --gid "${PRIMARY_GROUP_ID}" --groups "0,${USER_ID}" --shell /bin/bash --create-home "${USER_NAME}" 8 | } 9 | -------------------------------------------------------------------------------- /test/swift/test/a/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-argument-parser", 6 | "repositoryURL": "https://github.com/apple/swift-argument-parser.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "3d79b2b5a2e5af52c14e462044702ea7728f5770", 10 | "version": "0.1.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /test/swift/test/b/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-argument-parser", 6 | "repositoryURL": "https://github.com/apple/swift-argument-parser.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "3d79b2b5a2e5af52c14e462044702ea7728f5770", 10 | "version": "0.1.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /test/swift/test/c/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-argument-parser", 6 | "repositoryURL": "https://github.com/apple/swift-argument-parser.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "3d79b2b5a2e5af52c14e462044702ea7728f5770", 10 | "version": "0.1.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /src/cli/command/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cli } from 'clipanion'; 2 | import { describe, expect, test } from 'vitest'; 3 | import { registerCommands } from '.'; 4 | 5 | describe('cli/command/index', () => { 6 | test('exits with error', () => { 7 | const cli = new Cli({ binaryName: 'containerbase-cli' }); 8 | // @ts-expect-error - testing invalid mode 9 | expect(() => registerCommands(cli, 'invalid-mode')).not.toThrow(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/cli/tools/git/lfs.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../../install-tool/install-legacy-tool.service'; 3 | import { v2Tool } from '../../utils/v2-tool'; 4 | 5 | @injectable() 6 | @injectFromHierarchy() 7 | @v2Tool('git-lfs') 8 | export class GitLfsInstallService extends V2ToolInstallService { 9 | override readonly name = 'git-lfs'; 10 | override readonly parent = 'git'; 11 | } 12 | -------------------------------------------------------------------------------- /src/cli/tools/java/scala.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../../install-tool/install-legacy-tool.service'; 3 | import { v2Tool } from '../../utils/v2-tool'; 4 | 5 | @injectable() 6 | @injectFromHierarchy() 7 | @v2Tool('scala') 8 | export class ScalaInstallService extends V2ToolInstallService { 9 | override readonly name = 'scala'; 10 | override readonly parent = 'java'; 11 | } 12 | -------------------------------------------------------------------------------- /src/cli/command/index.ts: -------------------------------------------------------------------------------- 1 | import './cleanup-path'; 2 | import './file-download'; 3 | import './file-exists'; 4 | import './init-tool'; 5 | import './install-gem'; 6 | import './install-npm'; 7 | import './install-pip'; 8 | import './install-tool'; 9 | import './link-tool'; 10 | import './prepare-tool'; 11 | import './uninstall-gem'; 12 | import './uninstall-npm'; 13 | import './uninstall-pip'; 14 | import './uninstall-tool'; 15 | export { registerCommands } from './utils'; 16 | -------------------------------------------------------------------------------- /src/cli/utils/versions.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { isValid, parse } from './versions'; 3 | 4 | describe('cli/utils/versions', () => { 5 | test('isValid', () => { 6 | expect(isValid('1.0.0')).toBe(true); 7 | expect(isValid('abc')).toBe(false); 8 | }); 9 | 10 | test('parse', () => { 11 | expect(parse('1.0.0')).not.toBeNull(); 12 | expect(() => parse('abc')).toThrow('Invalid version: abc'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/jb/test/jsonnetfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/prometheus-operator/prometheus-operator.git", 8 | "subdir": "jsonnet/prometheus-operator" 9 | } 10 | }, 11 | "version": "cc6cb1ed7e58be6189bab001d239cd1df3ff9146", 12 | "sum": "J1G++A8hrtr3+OZQMmcNeb1w/C30bXqqwpwHL/Xhsd4=" 13 | } 14 | ], 15 | "legacyImports": false 16 | } 17 | -------------------------------------------------------------------------------- /test/golang/test/a/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.15.21/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= 2 | github.com/davecgh/go-spew v1.0.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 4 | github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 5 | github.com/pkg/errors v0.7.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 6 | -------------------------------------------------------------------------------- /test/golang/test/b/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.15.21/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= 2 | github.com/davecgh/go-spew v1.0.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 4 | github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 5 | github.com/pkg/errors v0.7.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 6 | -------------------------------------------------------------------------------- /test/ruby/test/b/Project/Podfile: -------------------------------------------------------------------------------- 1 | #plugin 'cocoapods-acknowledgements' 2 | 3 | target "Demo Project" do 4 | pod "CPDAcknowledgements", :path => "../CPDAcknowledgements.podspec" 5 | 6 | # These pods are used only for giving us some data 7 | pod "ORStackView" 8 | pod "IRFEmojiCheatSheet" 9 | 10 | target "Demo ProjectTests" do 11 | inherit! :search_paths 12 | 13 | pod 'Specta', '~> 1.0' 14 | pod 'Expecta', '~> 1.0' 15 | pod 'OCMockito', '~> 1.0' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/bin/v1-install-tool.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # shellcheck source=/dev/null 6 | . /usr/local/containerbase/util.sh 7 | 8 | function main() { 9 | local tool=${1} 10 | local version=${2} 11 | 12 | export "TOOL_NAME=${tool}" "TOOL_VERSION=${version}" 13 | # compability fallback 14 | export "$(get_tool_version_env "${tool}")=${version}" 15 | 16 | # shellcheck source=/dev/null 17 | . "${CONTAINERBASE_DIR}/tools/${tool}.sh" 18 | } 19 | 20 | main "$@" 21 | -------------------------------------------------------------------------------- /test/python/test/d-poetry/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "containerbase-test" 3 | version = "0.2.1" 4 | description = "Testing" 5 | authors = [] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = ">3.6" 10 | numpy = "^1.16" 11 | PyYAML = "^5.1" 12 | pampy = { version = "^0.3.0", python = ">3.6" } 13 | 14 | [tool.poetry.dev-dependencies] 15 | pytest = "^4.4" 16 | mock = "^3.0" 17 | 18 | [build-system] 19 | requires = ["poetry>=0.12"] 20 | build-backend = "poetry.masonry.api" 21 | -------------------------------------------------------------------------------- /test/python/test/c-poetry/pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | [tool.poetry] 3 | name = "containerbase-test" 4 | version = "0.2.1" 5 | description = "Testing" 6 | authors = [] 7 | license = "MIT" 8 | 9 | [tool.poetry.dependencies] 10 | python = ">3.13" 11 | numpy = "^1.21.3" 12 | PyYAML = "^6.0.1" 13 | pampy = "^0.2.1" 14 | 15 | [tool.poetry.dev-dependencies] 16 | pytest = "^7.1" 17 | mock = "^3.0" 18 | atomicwrites = "^1.3" 19 | 20 | [build-system] 21 | requires = ["poetry>=0.12"] 22 | build-backend = "poetry.masonry.api" 23 | -------------------------------------------------------------------------------- /src/cli/tools/node/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const NodeVersionMeta = z.object({ 4 | version: z.string(), 5 | lts: z.union([z.string(), z.boolean()]).optional(), 6 | }); 7 | export type NodeVersionMeta = z.infer; 8 | 9 | export const NpmPackageMetaList = z.array(NodeVersionMeta); 10 | 11 | export const NpmPackageMeta = z.object({ 12 | 'dist-tags': z.record(z.string()), 13 | name: z.string(), 14 | }); 15 | 16 | export type NpmPackageMeta = z.infer; 17 | -------------------------------------------------------------------------------- /.github/workflows/build-pr.yml: -------------------------------------------------------------------------------- 1 | name: build-pr 2 | 3 | on: 4 | pull_request: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.event.number || github.ref }} 8 | cancel-in-progress: true 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | if: ${{ github.event_name != 'pull_request' || github.repository_owner != github.event.pull_request.head.repo.owner.login }} 16 | uses: ./.github/workflows/build.yml 17 | permissions: 18 | contents: read 19 | checks: write 20 | id-token: write 21 | -------------------------------------------------------------------------------- /src/cli/utils/v2-tool.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { isKnownV2Tool, isNotKnownV2Tool, v2Tool } from './v2-tool'; 3 | 4 | describe('cli/utils/v2-tool', () => { 5 | @v2Tool('test-tool') 6 | class TestTool { 7 | readonly name = 'test-tool'; 8 | } 9 | 10 | const tool = new TestTool().name; 11 | 12 | test('isKnownV2Tool', () => { 13 | expect(isKnownV2Tool(tool)).toBe(true); 14 | }); 15 | 16 | test('isNotKnownV2Tool', () => { 17 | expect(isNotKnownV2Tool(tool)).toBe(false); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tools/utils.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import crypto from 'node:crypto'; 3 | import path from 'node:path'; 4 | 5 | /** 6 | * Writes `sum` compatible checksum file. 7 | * @param {string} file 8 | * @param {string} algorithm 9 | */ 10 | export async function hashFile(file, algorithm) { 11 | const data = await fs.readFile(file); 12 | const hash = crypto.createHash(algorithm); 13 | hash.update(data); 14 | await fs.writeFile( 15 | `${file}.${algorithm}`, 16 | `${hash.digest('hex')} ${path.basename(file)}`, 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/cli/install-tool/tool-version-resolver.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { EnvService, HttpService } from '../services'; 3 | 4 | export const TOOL_VERSION_RESOLVER = Symbol('TOOL_VERSION_RESOLVER'); 5 | 6 | @injectable() 7 | export abstract class ToolVersionResolver { 8 | abstract readonly tool: string; 9 | @inject(HttpService) 10 | protected readonly http!: HttpService; 11 | @inject(EnvService) 12 | protected readonly env!: EnvService; 13 | 14 | abstract resolve(version: string | undefined): Promise; 15 | } 16 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/bin/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -f "/usr/local/etc/env" && -z "${CONTAINERBASE_ENV+x}" ]]; then 4 | # shellcheck source=/dev/null 5 | . /usr/local/etc/env 6 | fi 7 | 8 | if [[ -n "${CONTAINERBASE_CLEANUP_PATH}" ]]; then 9 | # cleanup path via https://www.npmjs.com/package/del 10 | containerbase-cli cleanup path "${CONTAINERBASE_CLEANUP_PATH}" 11 | fi 12 | 13 | if [[ ! -d "/tmp/containerbase" ]]; then 14 | # initialize all prepared tools 15 | containerbase-cli init tool all 16 | fi 17 | 18 | exec dumb-init -- "$@" 19 | -------------------------------------------------------------------------------- /src/cli/utils/codes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Action not supported. 3 | */ 4 | export const NotSupported = 2; 5 | 6 | /** 7 | * A missing version is blocking installation. 8 | */ 9 | export const MissingVersion = 15; 10 | 11 | /** 12 | * A missing parent dependency blocks adding of child. 13 | */ 14 | export const MissingParent = 16; 15 | 16 | /** 17 | * Can't uninstall because the version of the tool is currently linked. 18 | */ 19 | export const CurrentVersion = 17; 20 | 21 | /** 22 | * A child dependency blocks removal of parent. 23 | */ 24 | export const BlockingChild = 18; 25 | -------------------------------------------------------------------------------- /test/latest/src/test/cert.conf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 2048 3 | default_md = sha256 4 | prompt = no 5 | encrypt_key = no 6 | distinguished_name = req_distinguished_name 7 | req_extensions = v3_req 8 | 9 | [ req_distinguished_name ] 10 | organizationName = Renovate test 11 | 12 | [ v3_req ] 13 | basicConstraints = CA:false 14 | keyUsage = critical, digitalSignature 15 | extendedKeyUsage = serverAuth 16 | subjectAltName = @sans 17 | 18 | 19 | [ sans ] 20 | DNS.1 = localhost 21 | DNS.2 = buildkitsandbox 22 | -------------------------------------------------------------------------------- /test/latest/src/test/ca.conf: -------------------------------------------------------------------------------- 1 | 2 | [ req ] 3 | default_bits = 2048 4 | default_md = sha256 5 | prompt = no 6 | encrypt_key = no 7 | distinguished_name = dn 8 | x509_extensions = v3_ca 9 | 10 | [ dn ] 11 | organizationName = Renovate 12 | commonName = Renovate ROOT CA 13 | 14 | [ v3_ca ] 15 | # Extensions for a typical CA (`man x509v3_config`). 16 | subjectKeyIdentifier = hash 17 | authorityKeyIdentifier = keyid:always,issuer 18 | basicConstraints = critical, CA:true 19 | keyUsage = critical, digitalSignature, cRLSign, keyCertSign 20 | -------------------------------------------------------------------------------- /test/swift/test/a/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "merc", 6 | platforms: [.macOS(.v11)], 7 | products: [.executable(name: "sample", targets: ["sample"])], 8 | dependencies: [ 9 | .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.1.0")), 10 | ], 11 | targets: [ 12 | .target( 13 | name: "sample", 14 | dependencies: [ 15 | .product(name: "ArgumentParser", package: "swift-argument-parser") 16 | ] 17 | ) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /test/swift/test/b/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "merc", 6 | platforms: [.macOS(.v11)], 7 | products: [.executable(name: "sample", targets: ["sample"])], 8 | dependencies: [ 9 | .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.2.2")), 10 | ], 11 | targets: [ 12 | .target( 13 | name: "sample", 14 | dependencies: [ 15 | .product(name: "ArgumentParser", package: "swift-argument-parser") 16 | ] 17 | ) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /test/swift/test/c/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "merc", 6 | platforms: [.macOS(.v10_15)], 7 | products: [.executable(name: "sample", targets: ["sample"])], 8 | dependencies: [ 9 | .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.2.2")), 10 | ], 11 | targets: [ 12 | .target( 13 | name: "sample", 14 | dependencies: [ 15 | .product(name: "ArgumentParser", package: "swift-argument-parser") 16 | ] 17 | ) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /patches/@semantic-release__github.patch: -------------------------------------------------------------------------------- 1 | diff --git a/lib/publish.js b/lib/publish.js 2 | index b91c39d1fd1f3c251eb3b1b29200921086437f90..abcc5716de218426494123c2d33c03c463ace8e8 100644 3 | --- a/lib/publish.js 4 | +++ b/lib/publish.js 5 | @@ -52,6 +52,7 @@ export default async function publish(pluginConfig, context, { Octokit }) { 6 | name: template(releaseNameTemplate)(context), 7 | body: template(releaseBodyTemplate)(context), 8 | prerelease: isPrerelease(branch), 9 | + make_latest: branch.type === "release" && branch.main && branch.prerelease ? "false" : "true", 10 | }; 11 | 12 | debug("release object: %O", release); 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@tsconfig/strictest/tsconfig.json", 4 | "@tsconfig/node20/tsconfig.json" 5 | ], 6 | "compilerOptions": { 7 | "baseUrl": ".", 8 | "allowJs": true, 9 | "noEmit": true, 10 | "types": ["node"], 11 | "verbatimModuleSyntax": true, 12 | "noImplicitOverride": true, 13 | "noPropertyAccessFromIndexSignature": false, 14 | "experimentalDecorators": true, 15 | "module": "ES2022", 16 | "moduleResolution": "Bundler", 17 | "paths": { 18 | "~test/*": ["./test/*"] 19 | } 20 | }, 21 | "exclude": ["node_modules", "bin", "dist", "coverage", "html"] 22 | } 23 | -------------------------------------------------------------------------------- /test/di.ts: -------------------------------------------------------------------------------- 1 | import { Cli } from 'clipanion'; 2 | import { Container } from 'inversify'; 3 | import { registerCommands } from '../src/cli/command'; 4 | import { createContainer, rootContainerModule } from '../src/cli/services'; 5 | import type { CliMode } from '../src/cli/utils'; 6 | 7 | export async function testContainer() { 8 | const parent = new Container(); 9 | await parent.load(rootContainerModule); 10 | return createContainer(parent); 11 | } 12 | 13 | export function testCli(mode: CliMode | null): Cli { 14 | const cli = new Cli({ binaryName: mode ?? 'containerbase-cli' }); 15 | registerCommands(cli, mode); 16 | return cli; 17 | } 18 | -------------------------------------------------------------------------------- /src/cli/utils/v2-tool.ts: -------------------------------------------------------------------------------- 1 | import type { ClazzDecorator } from './types'; 2 | 3 | const knownV2Tools = new Set(); 4 | 5 | export function isKnownV2Tool(tool: string): boolean { 6 | return knownV2Tools.has(tool); 7 | } 8 | export function isNotKnownV2Tool(tool: string): boolean { 9 | return !knownV2Tools.has(tool); 10 | } 11 | 12 | interface V2ToolInstallerService { 13 | prototype: { name: string }; 14 | } 15 | 16 | export function v2Tool(tool: string): ClazzDecorator { 17 | return (target: T): T | void => { 18 | knownV2Tools.add(tool); 19 | 20 | return target; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/cli/tools/rust.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../install-tool/install-legacy-tool.service'; 3 | import { V2ToolPrepareService } from '../prepare-tool/prepare-legacy-tools.service'; 4 | import { v2Tool } from '../utils/v2-tool'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | @v2Tool('rust') 9 | export class RustPrepareService extends V2ToolPrepareService { 10 | override readonly name = 'rust'; 11 | } 12 | 13 | @injectable() 14 | @injectFromHierarchy() 15 | @v2Tool('rust') 16 | export class RustInstallService extends V2ToolInstallService { 17 | override readonly name = 'rust'; 18 | } 19 | -------------------------------------------------------------------------------- /src/cli/install-tool/tool-version-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { injectable, multiInject } from 'inversify'; 2 | import { 3 | TOOL_VERSION_RESOLVER, 4 | type ToolVersionResolver, 5 | } from './tool-version-resolver'; 6 | 7 | @injectable() 8 | export class ToolVersionResolverService { 9 | constructor( 10 | @multiInject(TOOL_VERSION_RESOLVER) private resolver: ToolVersionResolver[], 11 | ) {} 12 | 13 | async resolve( 14 | tool: string, 15 | version: string | undefined, 16 | ): Promise { 17 | const resolver = this.resolver.find((r) => r.tool === tool); 18 | return (await resolver?.resolve(version)) ?? version; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/latest/src/etc/nginx/sites-enabled/default: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server; 4 | 5 | 6 | listen 443 ssl default_server; 7 | listen [::]:443 ssl default_server; 8 | 9 | ssl_certificate /test/renovate-chain.pem; 10 | ssl_certificate_key /test/renovate.key; 11 | 12 | root /var/www/html; 13 | 14 | # Add index.php to the list if you are using PHP 15 | index index.html index.htm index.nginx-debian.html; 16 | 17 | server_name _; 18 | 19 | location /-/ping { 20 | default_type application/json; 21 | return 200 "{}"; 22 | } 23 | 24 | location / { 25 | try_files $uri $uri/ =404; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/cli/tools/swift.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../install-tool/install-legacy-tool.service'; 3 | import { V2ToolPrepareService } from '../prepare-tool/prepare-legacy-tools.service'; 4 | import { v2Tool } from '../utils/v2-tool'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | @v2Tool('swift') 9 | export class SwiftPrepareService extends V2ToolPrepareService { 10 | override readonly name = 'swift'; 11 | } 12 | 13 | @injectable() 14 | @injectFromHierarchy() 15 | @v2Tool('swift') 16 | export class SwiftInstallService extends V2ToolInstallService { 17 | override readonly name = 'swift'; 18 | } 19 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/utils/v2/overrides.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # defines the legacy root directory where old tools will be installed 4 | # reguired for some legacy tools for bat test redirection 5 | export ROOT_DIR_LEGACY="${ROOT_DIR}" 6 | 7 | # OVERWRITE: 8 | # 9 | # defines the root directory where tools will be installed 10 | # shellcheck disable=SC2168,SC2034 11 | export ROOT_DIR=/opt/containerbase 12 | 13 | # get path location 14 | DIR="${BASH_SOURCE%/*}" 15 | if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi 16 | 17 | # source the helper files 18 | # shellcheck source=/dev/null 19 | . "${DIR}/filesystem.sh" 20 | # shellcheck source=/dev/null 21 | . "${DIR}/defaults.sh" 22 | -------------------------------------------------------------------------------- /src/cli/tools/golang.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../install-tool/install-legacy-tool.service'; 3 | import { V2ToolPrepareService } from '../prepare-tool/prepare-legacy-tools.service'; 4 | import { v2Tool } from '../utils/v2-tool'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | @v2Tool('golang') 9 | export class GolangPrepareService extends V2ToolPrepareService { 10 | override readonly name = 'golang'; 11 | } 12 | 13 | @injectable() 14 | @injectFromHierarchy() 15 | @v2Tool('golang') 16 | export class GolangInstallService extends V2ToolInstallService { 17 | override readonly name = 'golang'; 18 | } 19 | -------------------------------------------------------------------------------- /src/cli/tools/ruby/index.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../../install-tool/install-legacy-tool.service'; 3 | import { V2ToolPrepareService } from '../../prepare-tool/prepare-legacy-tools.service'; 4 | import { v2Tool } from '../../utils/v2-tool'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | @v2Tool('ruby') 9 | export class RubyPrepareService extends V2ToolPrepareService { 10 | override readonly name = 'ruby'; 11 | } 12 | 13 | @injectable() 14 | @injectFromHierarchy() 15 | @v2Tool('ruby') 16 | export class RubyInstallService extends V2ToolInstallService { 17 | override readonly name = 'ruby'; 18 | } 19 | -------------------------------------------------------------------------------- /src/cli/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface Distro { 2 | readonly name: string; 3 | readonly versionCode: string; 4 | readonly versionId: string; 5 | } 6 | 7 | export const cliModes = [ 8 | 'containerbase-cli', 9 | 'install-gem', 10 | 'install-npm', 11 | 'install-pip', 12 | 'install-tool', 13 | 'prepare-tool', 14 | 'uninstall-gem', 15 | 'uninstall-npm', 16 | 'uninstall-pip', 17 | 'uninstall-tool', 18 | ] as const; 19 | 20 | export type CliMode = (typeof cliModes)[number]; 21 | 22 | export type Arch = 'arm64' | 'amd64'; 23 | 24 | export type ClazzDecorator = (target: V) => V | void; 25 | 26 | export type InstallToolType = 'gem' | 'npm' | 'pip'; 27 | -------------------------------------------------------------------------------- /src/cli/tools/__mocks__/bun.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { BaseInstallService } from '../../install-tool/base-install.service'; 3 | import { spyable } from '~test/mock'; 4 | 5 | @injectable() 6 | @injectFromHierarchy() 7 | @spyable() 8 | export class BunInstallService extends BaseInstallService { 9 | readonly name = 'bun'; 10 | 11 | override install(_version: string): Promise { 12 | return Promise.resolve(); 13 | } 14 | 15 | override link(_version: string): Promise { 16 | return Promise.resolve(); 17 | } 18 | 19 | override uninstall(_version: string): Promise { 20 | return Promise.resolve(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/cli/tools/erlang/index.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../../install-tool/install-legacy-tool.service'; 3 | import { V2ToolPrepareService } from '../../prepare-tool/prepare-legacy-tools.service'; 4 | import { v2Tool } from '../../utils/v2-tool'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | @v2Tool('elixir') 9 | export class ErlangPrepareService extends V2ToolPrepareService { 10 | override readonly name = 'erlang'; 11 | } 12 | 13 | @injectable() 14 | @injectFromHierarchy() 15 | @v2Tool('erlang') 16 | export class ErlangInstallService extends V2ToolInstallService { 17 | override readonly name = 'erlang'; 18 | } 19 | -------------------------------------------------------------------------------- /src/cli/tools/python/index.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../../install-tool/install-legacy-tool.service'; 3 | import { V2ToolPrepareService } from '../../prepare-tool/prepare-legacy-tools.service'; 4 | import { v2Tool } from '../../utils/v2-tool'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | @v2Tool('python') 9 | export class PythonPrepareService extends V2ToolPrepareService { 10 | override readonly name = 'python'; 11 | } 12 | 13 | @injectable() 14 | @injectFromHierarchy() 15 | @v2Tool('python') 16 | export class PythonInstallService extends V2ToolInstallService { 17 | override readonly name = 'python'; 18 | } 19 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "containerbase", 3 | "build": { 4 | "dockerfile": "Dockerfile" 5 | }, 6 | "customizations": { 7 | "vscode": { 8 | "settings": { 9 | "terminal.integrated.profiles.linux": { 10 | "bash": { 11 | "path": "bash", 12 | "icon": "terminal-bash" 13 | } 14 | }, 15 | "terminal.integrated.defaultProfile.linux": "bash" 16 | }, 17 | "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 18 | } 19 | }, 20 | "postCreateCommand": "pnpm install", 21 | "mounts": [ 22 | "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /test/nix/test/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1659131907, 6 | "narHash": "sha256-8bz4k18M/FuVC+EVcI4aREN2PsEKT7LGmU2orfjnpCg=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "8d435fca5c561da8168abb30270788d2da2a7951", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /src/cli/tools/powershell.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../install-tool/install-legacy-tool.service'; 3 | import { V2ToolPrepareService } from '../prepare-tool/prepare-legacy-tools.service'; 4 | import { v2Tool } from '../utils/v2-tool'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | @v2Tool('powershell') 9 | export class PowershellPrepareService extends V2ToolPrepareService { 10 | override readonly name = 'powershell'; 11 | } 12 | 13 | @injectable() 14 | @injectFromHierarchy() 15 | @v2Tool('powershell') 16 | export class PowershellInstallService extends V2ToolInstallService { 17 | override readonly name = 'powershell'; 18 | } 19 | -------------------------------------------------------------------------------- /src/cli/main.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest'; 2 | import { main } from './main'; 3 | 4 | const mocks = vi.hoisted(() => ({ 5 | argv0: 'containerbase-cli', 6 | argv: ['node', 'containerbase-cli', 'help'], 7 | })); 8 | 9 | vi.mock('node:process', async (importOriginal) => ({ 10 | ...(await importOriginal()), 11 | ...mocks, 12 | })); 13 | 14 | vi.mock('./utils/common', async (importActual) => ({ 15 | ...(await importActual()), 16 | validateSystem: vi.fn(), 17 | })); 18 | 19 | describe('cli/main', () => { 20 | test('works', async () => { 21 | vi.spyOn(process.stdout, 'write').mockReturnValue(true); 22 | expect(await main()).toBeUndefined(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/cli/tools/java/sbt.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../../install-tool/install-legacy-tool.service'; 3 | import { V2ToolPrepareService } from '../../prepare-tool/prepare-legacy-tools.service'; 4 | import { v2Tool } from '../../utils/v2-tool'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | @v2Tool('sbt') 9 | export class SbtPrepareService extends V2ToolPrepareService { 10 | override readonly name = 'sbt'; 11 | } 12 | 13 | @injectable() 14 | @injectFromHierarchy() 15 | @v2Tool('sbt') 16 | export class SbtInstallService extends V2ToolInstallService { 17 | override readonly name = 'sbt'; 18 | override readonly parent = 'java'; 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/devcontainer.yml: -------------------------------------------------------------------------------- 1 | name: devcontainer 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | types: 7 | - opened 8 | - synchronize 9 | - reopened 10 | - ready_for_review 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | devcontainer-test: 17 | runs-on: ubuntu-24.04 18 | if: github.event.pull_request.draft != true 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 22 | 23 | - name: Build and run dev container task 24 | uses: devcontainers/ci@8bf61b26e9c3a98f69cb6ce2f88d24ff59b785c6 # v0.3.1900000417 25 | with: 26 | runCmd: pnpm build 27 | -------------------------------------------------------------------------------- /test/mock.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | export function spyable(): ClassDecorator { 4 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type 5 | return (target: T): T | void => { 6 | let proto = target.prototype; 7 | while ( 8 | proto && 9 | proto !== Function.prototype && 10 | proto !== Object.prototype 11 | ) { 12 | Object.getOwnPropertyNames(proto).forEach((key) => { 13 | if (key !== 'constructor' && typeof proto[key] === 'function') { 14 | target.prototype[key] = vi.spyOn(proto, key); 15 | } 16 | }); 17 | proto = Object.getPrototypeOf(proto); 18 | } 19 | return target; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /test/node/test/b/yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 6 6 | cacheKey: 8 7 | 8 | "semver@npm:7.3.2": 9 | version: 7.3.2 10 | resolution: "semver@npm:7.3.2" 11 | bin: 12 | semver: bin/semver.js 13 | checksum: 692f4900dadb43919614b0df9af23fe05743051cda0d1735b5e4d76f93c9e43a266fae73cfc928f5d1489f022c5c0e65dfd2900fcf5b1839c4e9a239729afa7b 14 | languageName: node 15 | linkType: hard 16 | 17 | "test@workspace:.": 18 | version: 0.0.0-use.local 19 | resolution: "test@workspace:." 20 | dependencies: 21 | semver: 7.3.2 22 | languageName: unknown 23 | linkType: soft 24 | -------------------------------------------------------------------------------- /src/cli/tools/erlang/elixir.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { V2ToolInstallService } from '../../install-tool/install-legacy-tool.service'; 3 | import { V2ToolPrepareService } from '../../prepare-tool/prepare-legacy-tools.service'; 4 | import { v2Tool } from '../../utils/v2-tool'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | @v2Tool('elixir') 9 | export class ElixirPrepareService extends V2ToolPrepareService { 10 | override readonly name = 'elixir'; 11 | } 12 | 13 | @injectable() 14 | @injectFromHierarchy() 15 | @v2Tool('elixir') 16 | export class ElixirInstallService extends V2ToolInstallService { 17 | override readonly name = 'elixir'; 18 | override readonly parent = 'erlang'; 19 | } 20 | -------------------------------------------------------------------------------- /src/cli/utils/hash.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'node:crypto'; 2 | import fs from 'node:fs/promises'; 3 | import type { LiteralUnion } from 'type-fest'; 4 | 5 | export type AlgorithmName = LiteralUnion< 6 | 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512', 7 | string 8 | >; 9 | 10 | export function hash(data: string | Buffer, algorithm: AlgorithmName): string { 11 | const hash = crypto.createHash(algorithm); 12 | hash.update(data); 13 | return hash.digest('hex'); 14 | } 15 | 16 | export async function hashFile( 17 | file: string, 18 | algorithm: AlgorithmName, 19 | ): Promise { 20 | const data = await fs.readFile(file); 21 | const hash = crypto.createHash(algorithm); 22 | hash.update(data); 23 | return hash.digest('hex'); 24 | } 25 | -------------------------------------------------------------------------------- /src/cli/proxy.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'node:process'; 2 | import { isNonEmptyString, isUndefined } from '@sindresorhus/is'; 3 | import { createGlobalProxyAgent } from 'global-agent'; 4 | 5 | const envVars = ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY']; 6 | 7 | export function bootstrap(): void { 8 | for (const envVar of envVars) { 9 | const lKey = envVar.toLowerCase(); 10 | if (isUndefined(env[envVar]) && isNonEmptyString(env[lKey])) { 11 | env[envVar] = env[lKey]; 12 | } 13 | 14 | if (env[envVar]) { 15 | env[lKey] = env[envVar]; 16 | } 17 | } 18 | 19 | if (isNonEmptyString(env.HTTP_PROXY) || isNonEmptyString(env.HTTPS_PROXY)) { 20 | createGlobalProxyAgent({ 21 | environmentVariableNamespace: '', 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tools/bats.js: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module'; 2 | import { Command, Option, runExit } from 'clipanion'; 3 | import spawn from 'nano-spawn'; 4 | 5 | const require = createRequire(import.meta.url); 6 | 7 | class BatsCommand extends Command { 8 | args = Option.Proxy(); 9 | 10 | async execute() { 11 | const bats = require.resolve('bats/bin/bats'); 12 | const batsAssert = require.resolve('bats-assert/load.bash'); 13 | const batsSupport = require.resolve('bats-support/load.bash'); 14 | 15 | await spawn(bats, this.args, { 16 | env: { 17 | BATS_ASSERT_LOAD_PATH: batsAssert, 18 | BATS_SUPPORT_LOAD_PATH: batsSupport, 19 | }, 20 | stdio: 'inherit', 21 | }); 22 | } 23 | } 24 | 25 | void runExit(BatsCommand); 26 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/tools/v2/vendir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function install_tool () { 4 | local versioned_tool_path 5 | local file 6 | local arch=linux-amd64 7 | 8 | if [[ "$(uname -p)" = "aarch64" ]]; then 9 | arch=linux-arm64 10 | fi 11 | 12 | file=$(get_from_url "https://github.com/vmware-tanzu/carvel-vendir/releases/download/v${TOOL_VERSION}/vendir-${arch}") 13 | 14 | versioned_tool_path=$(create_versioned_tool_path) 15 | create_folder "${versioned_tool_path}/bin" 16 | cp "${file}" "${versioned_tool_path}/bin/vendir" 17 | chmod +x "${versioned_tool_path}/bin/vendir" 18 | } 19 | 20 | function link_tool () { 21 | shell_wrapper "${TOOL_NAME}" "$(find_versioned_tool_path)/bin" 22 | } 23 | 24 | function test_tool () { 25 | vendir --version 26 | } 27 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'node:process'; 2 | import tsconfigPaths from 'vite-tsconfig-paths'; 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | const ci = !!env.CI; 6 | 7 | export default defineConfig({ 8 | plugins: [tsconfigPaths()], 9 | test: { 10 | coverage: { 11 | provider: 'v8', 12 | reporter: ci 13 | ? ['lcovonly', 'text'] 14 | : ['@containerbase/istanbul-reports-html', 'text'], 15 | include: ['src/cli/**/*.ts', '!**/__mocks__/**', '!**/types.ts'], 16 | }, 17 | reporters: ci 18 | ? ['default', 'github-actions', 'junit'] 19 | : ['default', 'html'], 20 | restoreMocks: true, 21 | setupFiles: './test/global-setup.ts', 22 | deps: { moduleDirectories: ['node_modules', '.yarn/'] }, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/tools/v2/scala.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function check_tool_requirements () { 4 | check_command java 5 | check_semver "$TOOL_VERSION" "all" 6 | } 7 | 8 | function install_tool () { 9 | local versioned_tool_path 10 | local file 11 | local URL='https://downloads.lightbend.com' 12 | 13 | file=$(get_from_url "${URL}/${TOOL_NAME}/${TOOL_VERSION}/${TOOL_NAME}-${TOOL_VERSION}.tgz") 14 | 15 | versioned_tool_path=$(create_versioned_tool_path) 16 | tar --strip 1 -C "${versioned_tool_path}" -xf "${file}" 17 | } 18 | 19 | function link_tool () { 20 | local versioned_tool_path 21 | versioned_tool_path=$(find_versioned_tool_path) 22 | 23 | shell_wrapper scala "${versioned_tool_path}/bin" 24 | } 25 | 26 | function test_tool () { 27 | scala --version 28 | } 29 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/tools/v2/terraform.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function install_tool () { 4 | local versioned_tool_path 5 | local file 6 | local arch=linux_amd64 7 | 8 | if [[ "$(uname -p)" = "aarch64" ]]; then 9 | arch=linux_arm64 10 | fi 11 | 12 | file=$(get_from_url "https://releases.hashicorp.com/terraform/${TOOL_VERSION}/terraform_${TOOL_VERSION}_${arch}.zip") 13 | 14 | versioned_tool_path=$(create_versioned_tool_path) 15 | create_folder "${versioned_tool_path}/bin" 16 | 17 | bsdtar -C "${versioned_tool_path}/bin" -xf "${file}" 18 | } 19 | 20 | function link_tool () { 21 | local versioned_tool_path 22 | versioned_tool_path=$(find_versioned_tool_path) 23 | 24 | shell_wrapper "${TOOL_NAME}" "${versioned_tool_path}/bin" 25 | terraform version 26 | } 27 | -------------------------------------------------------------------------------- /src/cli/utils/versions.ts: -------------------------------------------------------------------------------- 1 | import type SemVer from 'semver/classes/semver'; 2 | import semverCoerce from 'semver/functions/coerce'; 3 | import semverGte from 'semver/functions/gte'; 4 | import semverParse from 'semver/functions/parse'; 5 | import semverSatisfies from 'semver/functions/satisfies'; 6 | import semverSort from 'semver/functions/sort'; 7 | import semverValid from 'semver/functions/valid'; 8 | 9 | export { semverGte, semverSort, semverCoerce, semverSatisfies }; 10 | 11 | export function isValid(version: string): boolean { 12 | return semverValid(version) !== null; 13 | } 14 | 15 | export function parse(version: string | undefined): SemVer { 16 | const res = semverParse(version); 17 | if (!res) { 18 | throw new Error(`Invalid version: ${version}`); 19 | } 20 | return res; 21 | } 22 | -------------------------------------------------------------------------------- /src/cli/utils/hash.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { env } from 'node:process'; 3 | import { describe, expect, test } from 'vitest'; 4 | import { hash, hashFile } from './hash'; 5 | 6 | describe('cli/utils/hash', () => { 7 | test('should hash data with sha256', () => { 8 | expect(hash('https://example.com/test.txt', 'sha256')).toBe( 9 | 'd1dc63218c42abba594fff6450457dc8c4bfdd7c22acf835a50ca0e5d2693020', 10 | ); 11 | }); 12 | 13 | test('should hash file with sha256', async () => { 14 | const file = `${env.CONTAINERBASE_CACHE_DIR}/test.txt`; 15 | await fs.writeFile(file, 'https://example.com/test.txt'); 16 | expect(await hashFile(file, 'sha256')).toBe( 17 | 'd1dc63218c42abba594fff6450457dc8c4bfdd7c22acf835a50ca0e5d2693020', 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/golang/test/d/vendor/github.com/pkg/errors/appveyor.yml: -------------------------------------------------------------------------------- 1 | version: build-{build}.{branch} 2 | 3 | clone_folder: C:\gopath\src\github.com\pkg\errors 4 | shallow_clone: true # for startup speed 5 | 6 | environment: 7 | GOPATH: C:\gopath 8 | 9 | platform: 10 | - x64 11 | 12 | # http://www.appveyor.com/docs/installed-software 13 | install: 14 | # some helpful output for debugging builds 15 | - go version 16 | - go env 17 | # pre-installed MinGW at C:\MinGW is 32bit only 18 | # but MSYS2 at C:\msys64 has mingw64 19 | - set PATH=C:\msys64\mingw64\bin;%PATH% 20 | - gcc --version 21 | - g++ --version 22 | 23 | build_script: 24 | - go install -v ./... 25 | 26 | test_script: 27 | - set PATH=C:\gopath\bin;%PATH% 28 | - go test -v ./... 29 | 30 | #artifacts: 31 | # - path: '%GOPATH%\bin\*.exe' 32 | deploy: off 33 | -------------------------------------------------------------------------------- /test/path.ts: -------------------------------------------------------------------------------- 1 | import { join, sep } from 'node:path'; 2 | import { vi } from 'vitest'; 3 | 4 | export function cachePath(path: string): string { 5 | return `${globalThis.cacheDir}/${path}`.replace(/\/+/g, sep); 6 | } 7 | 8 | export function rootPath(path?: string): string { 9 | if (!path) { 10 | return globalThis.rootDir!.replace(/\/+/g, sep); 11 | } 12 | return join(globalThis.rootDir!, path).replace(/\/+/g, sep); 13 | } 14 | 15 | export async function ensurePaths(paths: string | string[]): Promise { 16 | const fs = 17 | await vi.importActual( 18 | 'node:fs/promises', 19 | ); 20 | for (const p of Array.isArray(paths) ? paths : [paths]) { 21 | const prepDir = rootPath(p); 22 | await fs.mkdir(prepDir, { 23 | recursive: true, 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/tools/git.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | require_root 4 | 5 | version_codename=$(get_distro) 6 | 7 | install -m 0755 -d /etc/apt/keyrings 8 | curl --retry 3 -fsSL -o /etc/apt/keyrings/git.asc \ 9 | 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0xF911AB184317630C59970973E363C90F8F1B6217' 10 | chmod a+r /etc/apt/keyrings/git.asc 11 | 12 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/git.asc] http://ppa.launchpad.net/git-core/ppa/ubuntu ${version_codename} main" | tee /etc/apt/sources.list.d/git.list 13 | 14 | 15 | # TODO: Only latest version available on launchpad :-/ 16 | #apt_install git=1:${TOOL_VERSION}* 17 | 18 | apt_install git 19 | 20 | # flutter workaround 21 | git config --system safe.directory "/opt/containerbase/tools/flutter/*" 22 | 23 | [[ -n $SKIP_VERSION ]] || git --version 24 | -------------------------------------------------------------------------------- /test/flutter/test/d/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: hello_world 2 | 3 | environment: 4 | sdk: '>=3.0.0 <4.0.0' 5 | 6 | dependencies: 7 | flutter: 8 | sdk: flutter 9 | 10 | collection: ^1.16.0 11 | meta: ^1.7.0 12 | typed_data: ^1.3.2 13 | vector_math: ^2.1.2 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | archive: ^3.4.2 20 | args: ^2.3.1 21 | async: ^2.8.2 22 | boolean_selector: ^2.1.0 23 | charcode: ^1.3.1 24 | convert: ^3.1.0 25 | crypto: ^3.0.2 26 | image: ^4.1.3 27 | matcher: ^0.12.11 28 | path: ^1.8.1 29 | pedantic: ^1.11.1 30 | petitparser: ^5.0.0 31 | quiver: ^3.2.1 32 | source_span: ^1.8.2 33 | stack_trace: ^1.10.0 34 | stream_channel: ^2.1.0 35 | string_scanner: ^1.1.0 36 | term_glyph: ^1.2.0 37 | test_api: '>=0.6.1' 38 | xml: ^6.1.0 39 | # PUBSPEC CHECKSUM: f789 40 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/tools/v2/jb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function install_tool () { 4 | local versioned_tool_path 5 | local file 6 | local arch=linux-amd64 7 | if [[ "$(uname -p)" = "aarch64" ]]; then 8 | arch=linux-arm64 9 | fi 10 | 11 | file=$(get_from_url "https://github.com/jsonnet-bundler/jsonnet-bundler/releases/download/v${TOOL_VERSION}/${TOOL_NAME}-${arch}") 12 | 13 | versioned_tool_path=$(create_versioned_tool_path) 14 | create_folder "${versioned_tool_path}/bin" 15 | cp "${file}" "${versioned_tool_path}/bin/jb" 16 | chmod +x "${versioned_tool_path}/bin/jb" 17 | } 18 | 19 | function link_tool () { 20 | local versioned_tool_path 21 | versioned_tool_path=$(find_versioned_tool_path) 22 | 23 | shell_wrapper "${TOOL_NAME}" "${versioned_tool_path}/bin" 24 | } 25 | 26 | function test_tool () { 27 | jb --version 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/trivy.yml: -------------------------------------------------------------------------------- 1 | name: trivy 2 | 3 | on: 4 | schedule: 5 | - cron: '59 11 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | trivy: 12 | runs-on: ubuntu-24.04 13 | permissions: 14 | contents: read 15 | security-events: write 16 | steps: 17 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 18 | with: 19 | show-progress: false 20 | 21 | - uses: aquasecurity/trivy-action@e5f43133f6e8736992c9f3c1b3296e24b37e17f2 # 0.10.0 22 | with: 23 | image-ref: 'ghcr.io/containerbase/base:latest' 24 | format: 'sarif' 25 | output: 'trivy-results.sarif' 26 | 27 | - uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 28 | with: 29 | sarif_file: trivy-results.sarif 30 | -------------------------------------------------------------------------------- /src/cli/services/compression.service.spec.ts: -------------------------------------------------------------------------------- 1 | import type { Container } from 'inversify'; 2 | import { beforeEach, describe, expect, test, vi } from 'vitest'; 3 | import { CompressionService } from '.'; 4 | import { testContainer } from '~test/di'; 5 | 6 | vi.mock('nano-spawn'); 7 | 8 | describe('cli/services/compression.service', () => { 9 | let child!: Container; 10 | 11 | beforeEach(async () => { 12 | child = await testContainer(); 13 | }); 14 | 15 | test('extracts with bstar', async () => { 16 | const svc = await child.getAsync(CompressionService); 17 | 18 | await expect( 19 | svc.extract({ file: 'some.txz', cwd: globalThis.cacheDir }), 20 | ).resolves.toBeUndefined(); 21 | 22 | await expect( 23 | svc.extract({ file: 'some.txz', cwd: globalThis.cacheDir, strip: 1 }), 24 | ).resolves.toBeUndefined(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/utils/linking.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # use this if custom env is required, creates a shell wrapper to /opt/containerbase/bin 4 | function shell_wrapper () { 5 | local SOURCE=$2 6 | if [[ -z "$SOURCE" ]]; then 7 | SOURCE=$(command -v "${1}") 8 | fi 9 | if [[ -d "$SOURCE" ]]; then 10 | SOURCE=$SOURCE/${1} 11 | fi 12 | check SOURCE true 13 | check_command "$SOURCE" 14 | containerbase-cli lt "$1" "$SOURCE" "$3" "$4" "$5" 15 | } 16 | 17 | # use this for simple symlink to /opt/containerbase/bin 18 | function link_wrapper () { 19 | local TARGET 20 | local SOURCE=$2 21 | TARGET="$(get_bin_path)/${1}" 22 | if [[ -z "$SOURCE" ]]; then 23 | SOURCE=$(command -v "${1}") 24 | fi 25 | if [[ -d "$SOURCE" ]]; then 26 | SOURCE=$SOURCE/${1} 27 | fi 28 | check_command "$SOURCE" 29 | ln -sf "$SOURCE" "$TARGET" 30 | } 31 | -------------------------------------------------------------------------------- /src/cli/tools/java/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | // https://api.adoptium.net/q/swagger-ui 4 | 5 | const AdoptiumVersionData = z.object({ 6 | semver: z.string(), 7 | }); 8 | 9 | export const AdoptiumReleaseVersions = z.object({ 10 | versions: z.array(AdoptiumVersionData), 11 | }); 12 | 13 | const AdoptiumPackage = z.object({ 14 | /** 15 | * sha256 checksum 16 | */ 17 | checksum: z.string(), 18 | link: z.string(), 19 | name: z.string(), 20 | }); 21 | 22 | export type AdoptiumPackage = z.infer; 23 | 24 | const AdoptiumBinary = z.object({ 25 | package: AdoptiumPackage, 26 | }); 27 | 28 | const AdoptiumRelease = z.object({ 29 | binaries: z.array(AdoptiumBinary), 30 | }); 31 | 32 | export const AdoptiumReleases = z.array(AdoptiumRelease); 33 | 34 | export const GradleVersionData = z.object({ 35 | version: z.string(), 36 | }); 37 | -------------------------------------------------------------------------------- /src/cli/command/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'node:process'; 2 | import { beforeEach, describe, expect, test } from 'vitest'; 3 | import { getVersion, isToolIgnored } from './utils'; 4 | 5 | describe('cli/command/utils', () => { 6 | beforeEach(() => { 7 | delete env.NODE_VERSION; 8 | delete env.DEL_CLI_VERSION; 9 | env.IGNORED_TOOLS = 'php,pnpm'; 10 | }); 11 | 12 | test('getVersion', () => { 13 | expect(getVersion('node')).toBeUndefined(); 14 | env.NODE_VERSION = '1.0.0'; 15 | expect(getVersion('node')).toBe('1.0.0'); 16 | env.DEL_CLI_VERSION = '1.0.1'; 17 | expect(getVersion('del-cli')).toBe('1.0.1'); 18 | }); 19 | 20 | test('isToolIgnored', async () => { 21 | expect(await isToolIgnored('node')).toBe(false); 22 | expect(await isToolIgnored('pnpm')).toBe(true); 23 | expect(await isToolIgnored('php')).toBe(true); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/flutter/test/a/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_view 2 | description: A new flutter project. 3 | 4 | environment: 5 | # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite. 6 | sdk: '>=2.0.0-dev.68.0 <3.0.0' 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | 12 | collection: ^1.14.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" 13 | meta: ^1.2.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" 14 | typed_data: ^1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" 15 | vector_math: ^2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" 16 | 17 | flutter: 18 | uses-material-design: true 19 | assets: 20 | - assets/flutter-mark-square-64.png 21 | # PUBSPEC CHECKSUM: 643e 22 | -------------------------------------------------------------------------------- /patches/clipanion@3.2.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/lib/platform/package.json b/lib/platform/package.json 2 | index 5ea9d43740d1bdb509612376c0e9ce91d20f8b20..4164c61b7a30b2d74536f1f34e6cca6e6ed8c2f9 100644 3 | --- a/lib/platform/package.json 4 | +++ b/lib/platform/package.json 5 | @@ -1,4 +1,5 @@ 6 | { 7 | - "main": "./node", 8 | - "browser": "./browser" 9 | + "main": "./node.js", 10 | + "browser": "./browser.js", 11 | + "module": "./node.mjs" 12 | } 13 | diff --git a/package.json b/package.json 14 | index cbf943bcc31a864f2771e457d327e5106eba9afe..ae653069c2fd2dadcc4d1df45cdb8b25cf664e1a 100644 15 | --- a/package.json 16 | +++ b/package.json 17 | @@ -13,7 +13,8 @@ 18 | "command" 19 | ], 20 | "version": "3.2.1", 21 | - "main": "lib/advanced/index", 22 | + "main": "lib/advanced/index.js", 23 | + "module": "lib/advanced/index.mjs", 24 | "license": "MIT", 25 | "sideEffects": false, 26 | "repository": { 27 | -------------------------------------------------------------------------------- /src/cli/command/init-tool.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cli } from 'clipanion'; 2 | import { describe, expect, test, vi } from 'vitest'; 3 | import { registerCommands } from '.'; 4 | 5 | const mocks = vi.hoisted(() => ({ 6 | installTool: vi.fn(), 7 | prepareTools: vi.fn(), 8 | initializeTools: vi.fn(), 9 | })); 10 | 11 | vi.mock('../install-tool', () => mocks); 12 | vi.mock('../prepare-tool', () => mocks); 13 | 14 | describe('cli/command/init-tool', () => { 15 | test('init-tool', async () => { 16 | const cli = new Cli({ binaryName: 'cli' }); 17 | registerCommands(cli, null); 18 | 19 | expect(await cli.run(['init', 'tool', 'node'])).toBe(0); 20 | expect(mocks.initializeTools).toHaveBeenCalledExactlyOnceWith( 21 | ['node'], 22 | false, 23 | ); 24 | 25 | mocks.initializeTools.mockRejectedValueOnce(new Error('test')); 26 | expect(await cli.run(['init', 'tool', 'node'])).toBe(1); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /.markdownlint-cli2.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "extends": "markdownlint/style/prettier", 4 | "list-marker-space": { 5 | "ul_multi": 1, 6 | "ul_single": 1, 7 | }, 8 | "ul-indent": { 9 | "indent": 2, 10 | }, 11 | 12 | // Disable some built-in rules 13 | "no-emphasis-as-heading": false, 14 | "first-line-heading": false, 15 | "line-length": false, 16 | "no-emphasis-as-header": false, 17 | "no-inline-html": false, 18 | "single-h1": false, 19 | "no-duplicate-heading": { 20 | "siblings_only": true, 21 | }, 22 | }, 23 | 24 | // Define glob expressions to use (only valid at root) 25 | // "globs": ["**/*.md"], 26 | 27 | // Define glob expressions to ignore 28 | "ignores": [ 29 | ".yarn", 30 | "**/node_modules/**", 31 | "**/TestResults/**", 32 | "**/bin/**", 33 | "**/obj/**", 34 | "coverage/", 35 | ".pnpm-store", 36 | ], 37 | } 38 | -------------------------------------------------------------------------------- /src/cli/command/cleanup-path.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cli } from 'clipanion'; 2 | import { describe, expect, test, vi } from 'vitest'; 3 | import { registerCommands } from '.'; 4 | 5 | const mocks = vi.hoisted(() => ({ 6 | deleteAsync: vi.fn(), 7 | })); 8 | 9 | vi.mock('del', () => mocks); 10 | 11 | describe('cli/command/cleanup-path', () => { 12 | test('works', async () => { 13 | const cli = new Cli({ binaryName: 'containerbase-cli' }); 14 | registerCommands(cli, null); 15 | 16 | expect( 17 | await cli.run(['cleanup', 'path', '/tmp/**:/var/tmp', '/some/path/**']), 18 | ).toBe(0); 19 | 20 | expect(mocks.deleteAsync).toHaveBeenCalledExactlyOnceWith( 21 | ['/tmp/**', '/var/tmp', '/some/path/**'], 22 | { dot: true }, 23 | ); 24 | 25 | mocks.deleteAsync.mockRejectedValueOnce(new Error('test')); 26 | expect( 27 | await cli.run(['cleanup', 'path', '/tmp/**:/var/tmp', '/some/path/**']), 28 | ).toBe(1); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/nix/test/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "update-flake-lock"; 3 | 4 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 5 | 6 | outputs = 7 | { self 8 | , nixpkgs 9 | }: 10 | let 11 | nameValuePair = name: value: { inherit name value; }; 12 | genAttrs = names: f: builtins.listToAttrs (map (n: nameValuePair n (f n)) names); 13 | 14 | allSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 15 | forAllSystems = f: genAttrs allSystems 16 | (system: f { 17 | inherit system; 18 | pkgs = import nixpkgs { inherit system; }; 19 | }); 20 | in 21 | { 22 | devShell = forAllSystems 23 | ({ system, pkgs, ... }: 24 | pkgs.stdenv.mkDerivation { 25 | name = "update-flake-lock-devshell"; 26 | buildInputs = [ pkgs.shellcheck ]; 27 | src = self; 28 | }); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/cli/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { argv0 } from 'node:process'; 2 | import nanoSpawn, { type Options, type Subprocess } from 'nano-spawn'; 3 | import { type CliMode, cliModes } from './types'; 4 | 5 | export type * from './types'; 6 | export * from './versions'; 7 | export * from './logger'; 8 | export * from './common'; 9 | export type { Options as SpawnOptions, Subprocess as SpawnResult }; 10 | 11 | export function cliMode(): CliMode | null { 12 | for (const mode of cliModes) { 13 | if (argv0.endsWith(`/${mode}`) || argv0 === mode) { 14 | return mode; 15 | } 16 | } 17 | 18 | // Test mode 19 | if (argv0.endsWith(`/node`) || argv0 === 'node') { 20 | return 'containerbase-cli'; 21 | } 22 | 23 | return null; 24 | } 25 | 26 | export async function spawn( 27 | cmd: string, 28 | args: string[], 29 | options?: Options, 30 | ): Promise { 31 | return await nanoSpawn(cmd, args, { 32 | stdio: ['inherit', 'inherit', 1], 33 | ...options, 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/cli/tools/python/pip.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { ToolVersionResolver } from '../../install-tool/tool-version-resolver'; 3 | import { PypiJson } from './schema'; 4 | 5 | @injectable() 6 | export abstract class PipVersionResolver extends ToolVersionResolver { 7 | async resolve(version: string | undefined): Promise { 8 | if (version === undefined || version === 'latest') { 9 | const meta = await this.fetchMeta(this.tool); 10 | return meta.info.version; 11 | } 12 | return version; 13 | } 14 | 15 | protected async fetchMeta(tool: string): Promise { 16 | return PypiJson.parse( 17 | await this.http.getJson( 18 | `https://pypi.org/pypi/${normalizePythonDepName(tool)}/json`, 19 | ), 20 | ); 21 | } 22 | } 23 | 24 | // https://packaging.python.org/en/latest/specifications/name-normalization/ 25 | export function normalizePythonDepName(name: string): string { 26 | return name.replace(/[-_.]+/g, '-').toLowerCase(); 27 | } 28 | -------------------------------------------------------------------------------- /src/cli/services/compression.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { spawn } from '../utils'; 3 | import { EnvService } from './env.service'; 4 | 5 | export interface ExtractConfig { 6 | file: string; 7 | cwd: string; 8 | strip?: number | undefined; 9 | 10 | files?: string[]; 11 | 12 | /** 13 | * Additional options to pass to the `bsdtar` command. 14 | */ 15 | options?: string[]; 16 | } 17 | 18 | @injectable() 19 | export class CompressionService { 20 | @inject(EnvService) 21 | private readonly envSvc!: EnvService; 22 | 23 | async extract({ 24 | file, 25 | cwd, 26 | strip, 27 | files, 28 | options, 29 | }: ExtractConfig): Promise { 30 | await spawn('bsdtar', [ 31 | '-xf', 32 | file, 33 | '-C', 34 | cwd, 35 | ...(strip ? ['--strip', `${strip}`] : []), 36 | '--uid', 37 | `${this.envSvc.userId}`, 38 | '--gid', 39 | '0', 40 | ...(options ?? []), 41 | ...(files ?? []), 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/ruby/test/b/CPDAcknowledgements.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "CPDAcknowledgements" 3 | s.version = "1.0.0" 4 | s.summary = "Show your CocoaPods dependencies in-app." 5 | s.description = <<-DESC 6 | Show your CocoaPods library and contributors in-app with smart defaults, and customisable view controllers. 7 | DESC 8 | s.homepage = "https://github.com/CocoaPods/CPDAcknowledgements" 9 | s.license = 'MIT' 10 | s.author = { "Orta Therox" => "orta.therox@gmail.com", "Fabio Pelosin" => "fabiopelosin@gmail.com" } 11 | s.source = { :git => "https://github.com/CocoaPods/CPDAcknowledgements.git", :tag => s.version.to_s } 12 | s.homepage = "https://github.com/CocoaPods/CPDAcknowledgements" 13 | s.social_media_url = "https://twitter.com/CocoaPods" 14 | s.ios.deployment_target = '8.0' 15 | s.source_files = 'CPDAcknowledgements/**/**' 16 | s.private_header_files = 'CPDAcknowledgements/private/*.h' 17 | s.ios.frameworks = 'UIKit' 18 | end 19 | -------------------------------------------------------------------------------- /test/bash/cache.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create a temp directory for the test or use 4 | # the global one if defined in the given var. 5 | # Returns the path to the temp directory 6 | # 7 | # The caller is responible to create the 8 | # folder in the given var if it is set 9 | function create_temp_dir () { 10 | local global_var=$1 11 | 12 | if [[ -z "${!global_var}" ]]; then 13 | temp_dir="$(mktemp -u)" 14 | # shellcheck disable=SC2174 15 | mkdir -m 777 -p "${temp_dir}" >/dev/null 2>&1 16 | echo "${temp_dir}" 17 | else 18 | echo "${!global_var}" 19 | fi 20 | } 21 | 22 | # Removes the temp dir in the first var 23 | # if it is created for the test 24 | # If the global env is set, nothing will be done 25 | function clean_temp_dir () { 26 | local temp_dir=$1 27 | local global_var=$2 28 | 29 | if [[ -z "${!global_var}" ]]; then 30 | rm -rf "${temp_dir}" 31 | fi 32 | } 33 | 34 | # generates a random word to be used in tests 35 | function random_word () { 36 | tr -dc A-Za-z0-9 /**"], 12 | "program": "${workspaceFolder}\\src\\cli\\index.ts", 13 | "outFiles": ["${workspaceFolder}/**/*.js"] 14 | }, 15 | { 16 | "type": "node", 17 | "request": "launch", 18 | "name": "Debug Current Test File", 19 | "autoAttachChildProcesses": true, 20 | "skipFiles": ["/**", "**/node_modules/**"], 21 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 22 | "args": [ 23 | "run", 24 | "--pool=forks", 25 | "--poolOptions.forks.singleFork", 26 | "${relativeFile}" 27 | ], 28 | "smartStep": true, 29 | "console": "integratedTerminal" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/cli/command/uninstall-gem.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'clipanion'; 2 | import { UninstallToolCommand } from './uninstall-tool'; 3 | import { command } from './utils'; 4 | 5 | @command('containerbase-cli') 6 | export class UninstallGemCommand extends UninstallToolCommand { 7 | static override paths = [['uninstall', 'gem']]; 8 | static override usage = Command.Usage({ 9 | description: 'Uninstalls a gem package from the container.', 10 | examples: [ 11 | ['Uninstalls rake v13.0.6', '$0 uninstall gem rake 13.0.6'], 12 | ['Uninstalls all rake versions', '$0 uninstall gem rake --all'], 13 | ], 14 | }); 15 | 16 | protected override type = 'gem' as const; 17 | } 18 | 19 | @command('uninstall-gem') 20 | export class UninstallGemShortCommand extends UninstallGemCommand { 21 | static override paths = [Command.Default]; 22 | static override usage = Command.Usage({ 23 | description: 'Uninstalls a gem package from the container.', 24 | examples: [ 25 | ['Uninstalls rake v13.0.6', '$0 rake 13.0.6'], 26 | ['Uninstalls all rake versions', '$0 rake --all'], 27 | ], 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/cli/command/uninstall-npm.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'clipanion'; 2 | import { UninstallToolCommand } from './uninstall-tool'; 3 | import { command } from './utils'; 4 | 5 | @command('containerbase-cli') 6 | export class UninstallNpmCommand extends UninstallToolCommand { 7 | static override paths = [['uninstall', 'npm']]; 8 | static override usage = Command.Usage({ 9 | description: 'Uninstalls a npm package from the container.', 10 | examples: [ 11 | ['Uninstalls del-cli v5.0.0', '$0 uninstall npm del-cli 5.0.0'], 12 | ['Uninstalls all del-cli versions', '$0 uninstall npm del-cli --all'], 13 | ], 14 | }); 15 | 16 | protected override type = 'npm' as const; 17 | } 18 | 19 | @command('uninstall-npm') 20 | export class UninstallNpmShortCommand extends UninstallNpmCommand { 21 | static override paths = [Command.Default]; 22 | static override usage = Command.Usage({ 23 | description: 'Uninstalls a npm package from the container.', 24 | examples: [ 25 | ['Uninstalls del-cli v5.0.0', '$0 del-cli 5.0.0'], 26 | ['Uninstalls all del-cli versions', '$0 del-cli --all'], 27 | ], 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/cli/command/uninstall-pip.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'clipanion'; 2 | import { UninstallToolCommand } from './uninstall-tool'; 3 | import { command } from './utils'; 4 | 5 | @command('containerbase-cli') 6 | export class UninstallPipCommand extends UninstallToolCommand { 7 | static override paths = [['uninstall', 'pip']]; 8 | static override usage = Command.Usage({ 9 | description: 'Uninstalls a pip package from the container.', 10 | examples: [ 11 | ['Uninstalls checkov v2.4.7', '$0 uninstall pip checkov 2.4.7'], 12 | ['Uninstalls all checkov versions', '$0 uninstall pip checkov --all'], 13 | ], 14 | }); 15 | 16 | protected override type = 'pip' as const; 17 | } 18 | 19 | @command('uninstall-pip') 20 | export class UninstallPipShortCommand extends UninstallPipCommand { 21 | static override paths = [Command.Default]; 22 | static override usage = Command.Usage({ 23 | description: 'Uninstalls a pip package from the container.', 24 | examples: [ 25 | ['Uninstalls checkov v2.4.7', '$0 checkov 2.4.7'], 26 | ['Uninstalls all checkov versions', '$0 checkov --all'], 27 | ], 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/cli/main.ts: -------------------------------------------------------------------------------- 1 | import { argv, argv0, version } from 'node:process'; 2 | import { Builtins, Cli } from 'clipanion'; 3 | import { registerCommands } from './command'; 4 | import { bootstrap } from './proxy'; 5 | import { cliMode, logger, parseBinaryName, validateSystem } from './utils'; 6 | 7 | declare global { 8 | // needs to be this to make eslint happy 9 | var CONTAINERBASE_VERSION: string | undefined; 10 | } 11 | 12 | export async function main(): Promise { 13 | logger.trace({ argv0, argv, version }, 'main'); 14 | bootstrap(); 15 | await validateSystem(); 16 | 17 | const mode = cliMode(); 18 | const [node, app, ...args] = argv; 19 | 20 | const cli = new Cli({ 21 | binaryLabel: `containerbase-cli`, 22 | binaryName: parseBinaryName(mode, node!, app!)!, 23 | binaryVersion: `${ 24 | globalThis.CONTAINERBASE_VERSION ?? '0.0.0-PLACEHOLDER' 25 | } (Node ${version})`, 26 | }); 27 | 28 | cli.register(Builtins.DefinitionsCommand); 29 | cli.register(Builtins.HelpCommand); 30 | cli.register(Builtins.VersionCommand); 31 | 32 | registerCommands(cli, mode); 33 | 34 | await cli.runExit(args); 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 WhiteSource Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/cli/prepare-tool/base-prepare.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { EnvService, PathService } from '../services'; 3 | import { NoInitTools, NoPrepareTools } from '../tools'; 4 | import { type SpawnOptions, type SpawnResult, spawn } from '../utils'; 5 | 6 | @injectable() 7 | export abstract class BasePrepareService { 8 | @inject(PathService) 9 | protected readonly pathSvc!: PathService; 10 | @inject(EnvService) 11 | protected readonly envSvc!: EnvService; 12 | 13 | abstract readonly name: string; 14 | 15 | prepare(): Promise | void { 16 | // noting to do; 17 | } 18 | initialize(): Promise | void { 19 | // noting to do; 20 | } 21 | 22 | needsInitialize(): boolean { 23 | return !NoInitTools.includes(this.name); 24 | } 25 | 26 | needsPrepare(): boolean { 27 | return !NoPrepareTools.includes(this.name); 28 | } 29 | 30 | toString(): string { 31 | return this.name; 32 | } 33 | 34 | protected _spawn( 35 | command: string, 36 | args: string[], 37 | options?: SpawnOptions, 38 | ): Promise { 39 | return spawn(command, args, { cwd: this.envSvc.tmpDir, ...options }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/utils/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Will set the version of the given tool to the given version in the versions folder 4 | function set_tool_version () { 5 | local tool=${1:-$TOOL_NAME} 6 | local version=${2:-$TOOL_VERSION} 7 | 8 | check tool true 9 | check version true 10 | 11 | local version_path 12 | version_path=$(get_version_path) 13 | 14 | # set umask for subshell and enter version 15 | # will only affect if we write the file initially 16 | # umask 117 -> chmod 660 17 | (umask 117 && echo "${version}" > "${version_path}/${tool}") 18 | } 19 | 20 | # Gets the version of the tool behind $TOOL_NAME or the first argument 21 | # if it is set, empty otherwise 22 | function get_tool_version () { 23 | local version_path 24 | local tool=${1:-$TOOL_NAME} 25 | check tool 26 | 27 | version_path=$(get_version_path) 28 | 29 | cat "${version_path}/${tool}" 2>&- || true 30 | } 31 | 32 | # Gets the version env var for the given tool 33 | # e.g 34 | # get_tool_version_env foo-bar 35 | # returns 36 | # FOO_BAR_VERSION 37 | function get_tool_version_env () { 38 | local tool=${1//-/_} 39 | check tool true 40 | 41 | tool=${tool^^}_VERSION 42 | echo "${tool}" 43 | } 44 | -------------------------------------------------------------------------------- /src/cli/utils/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest'; 2 | import { cliMode } from '.'; 3 | 4 | const procMocks = vi.hoisted(() => ({ argv0: '', env: {} })); 5 | vi.mock('node:process', () => procMocks); 6 | 7 | describe('cli/utils/index', () => { 8 | test('cliMode', async () => { 9 | expect(cliMode()).toBeNull(); 10 | procMocks.argv0 = 'containerbase-cli'; 11 | expect((await import('.')).cliMode()).toBe('containerbase-cli'); 12 | procMocks.argv0 = 'install-gem'; 13 | expect((await import('.')).cliMode()).toBe('install-gem'); 14 | procMocks.argv0 = 'install-npm'; 15 | expect((await import('.')).cliMode()).toBe('install-npm'); 16 | procMocks.argv0 = 'install-pip'; 17 | expect((await import('.')).cliMode()).toBe('install-pip'); 18 | procMocks.argv0 = 'install-tool'; 19 | expect((await import('.')).cliMode()).toBe('install-tool'); 20 | procMocks.argv0 = 'prepare-tool'; 21 | expect((await import('.')).cliMode()).toBe('prepare-tool'); 22 | procMocks.argv0 = '/usr/bin/node'; 23 | expect((await import('.')).cliMode()).toBe('containerbase-cli'); 24 | procMocks.argv0 = '/bin/sh'; 25 | expect((await import('.')).cliMode()).toBeNull(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/bin/v2-install-tool.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # shellcheck source=/dev/null 6 | . /usr/local/containerbase/util.sh 7 | 8 | # shellcheck source=/dev/null 9 | . "${CONTAINERBASE_DIR}/utils/v2/overrides.sh" 10 | 11 | function main() { 12 | local mode=${1} 13 | local tool=${2} 14 | local version=${3:-} 15 | 16 | export "TOOL_NAME=${tool}" 17 | 18 | if [[ -n "${version}" ]]; then 19 | export "TOOL_VERSION=${version}" 20 | # compability fallback 21 | export "$(get_tool_version_env "${tool}")=${version}" 22 | fi 23 | 24 | # shellcheck source=/dev/null 25 | . "${CONTAINERBASE_DIR}/tools/v2/${tool}.sh" 26 | 27 | case "$mode" in 28 | prepare) 29 | prepare_tool 30 | ;; 31 | init) 32 | init_tool 33 | ;; 34 | check) 35 | check_tool_requirements 36 | ;; 37 | install) 38 | check_tool_requirements 39 | install_tool 40 | ;; 41 | link) 42 | check_tool_requirements 43 | link_tool 44 | ;; 45 | post-install) 46 | check_tool_requirements 47 | post_install 48 | ;; 49 | test) 50 | test_tool 51 | ;; 52 | uninstall) 53 | uninstall_tool 54 | ;; 55 | esac 56 | 57 | } 58 | 59 | main "$@" 60 | -------------------------------------------------------------------------------- /src/cli/command/utils.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'node:process'; 2 | import type { Cli, CommandClass } from 'clipanion'; 3 | import { EnvService, createContainer } from '../services'; 4 | import { type CliMode, logger } from '../utils'; 5 | 6 | export function getVersion(tool: string): string | undefined { 7 | return env[tool.replace('-', '_').toUpperCase() + '_VERSION']; 8 | } 9 | 10 | export async function isToolIgnored(tool: string): Promise { 11 | const container = createContainer(); 12 | return (await container.getAsync(EnvService)).isToolIgnored(tool); 13 | } 14 | 15 | const commands: Record = {} as never; 16 | 17 | type CommandDecorator = ( 18 | target: T, 19 | ) => T | void; 20 | 21 | export function command(mode: CliMode): CommandDecorator { 22 | return (target: T): T | void => { 23 | commands[mode] ??= []; 24 | commands[mode].push(target); 25 | 26 | return target; 27 | }; 28 | } 29 | 30 | export function registerCommands(cli: Cli, mode: CliMode | null): void { 31 | logger.debug('prepare commands'); 32 | for (const command of commands[mode ?? 'containerbase-cli'] ?? []) { 33 | cli.register(command); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/cli/services/link-tool.service.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { beforeAll, describe, expect, test, vi } from 'vitest'; 3 | import { ensurePaths } from '../../../test/path'; 4 | import { createContainer } from '../services'; 5 | import { LinkToolService } from './link-tool.service'; 6 | 7 | describe('cli/services/link-tool.service', async () => { 8 | const child = createContainer(); 9 | child.bind(LinkToolService).toSelf(); 10 | const svc = await child.getAsync(LinkToolService); 11 | 12 | beforeAll(async () => { 13 | await ensurePaths('opt/containerbase/bin'); 14 | }); 15 | 16 | test('shell-wrapper', async () => { 17 | const spy = vi.spyOn(fs, 'writeFile'); 18 | await expect( 19 | svc.shellwrapper('node', { 20 | srcDir: '/bin/bash', 21 | extraToolEnvs: ['core'], 22 | exports: 'T=1', 23 | body: '# dummy', 24 | args: '-c', 25 | }), 26 | ).resolves.toBeUndefined(); 27 | 28 | expect(spy).toHaveBeenCalledOnce(); 29 | 30 | spy.mockClear(); 31 | await expect( 32 | svc.shellwrapper('node', { 33 | srcDir: 'bin', 34 | }), 35 | ).resolves.toBeUndefined(); 36 | expect(spy).toHaveBeenCalledOnce(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/cli/tools/php/__mocks__/composer.ts: -------------------------------------------------------------------------------- 1 | import { injectFromHierarchy, injectable } from 'inversify'; 2 | import { BaseInstallService } from '../../../install-tool/base-install.service'; 3 | import { ToolVersionResolver } from '../../../install-tool/tool-version-resolver'; 4 | 5 | @injectable() 6 | @injectFromHierarchy() 7 | export class ComposerVersionResolver extends ToolVersionResolver { 8 | readonly tool = 'composer'; 9 | 10 | resolve(version: string | undefined): Promise { 11 | return Promise.resolve(version); 12 | } 13 | } 14 | 15 | @injectable() 16 | @injectFromHierarchy() 17 | export class ComposerInstallService extends BaseInstallService { 18 | readonly name = 'composer'; 19 | 20 | override isInstalled(_version: string): Promise { 21 | return Promise.resolve(false); 22 | } 23 | 24 | override install(_version: string): Promise { 25 | return Promise.resolve(); 26 | } 27 | 28 | override link(_version: string): Promise { 29 | return Promise.resolve(); 30 | } 31 | 32 | override test(_version: string): Promise { 33 | return Promise.resolve(); 34 | } 35 | 36 | override uninstall(_version: string): Promise { 37 | return Promise.resolve(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/cli/tools/python/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { logger } from '../../utils'; 3 | 4 | // function fixPythonVersion(version: string): string { 5 | // return version; //.replace('\u003C', '<').replace('\u003E', '>'); 6 | // } 7 | 8 | const depRe = /^(?[a-z-]+)(?\[[a-z,]+\])?(?.+)(?:$|;)/; 9 | 10 | function parseDep(dep: string): [string, string] | null { 11 | const groups = depRe.exec(dep)?.groups; 12 | if (!groups) { 13 | logger.debug({ dep }, 'Failed to parse dependency'); 14 | return null; 15 | } 16 | return [groups.name!, groups.version!]; 17 | } 18 | 19 | const PypiRelease = z.object({ 20 | packagetype: z.enum(['sdist', 'bdist_wheel', 'unknown']).catch('unknown'), 21 | requires_python: z.string().nullish(), 22 | yanked: z.boolean(), 23 | }); 24 | 25 | export const PypiJson = z.object({ 26 | info: z.object({ 27 | version: z.string(), 28 | requires_python: z.string().nullish(), 29 | requires_dist: z 30 | .array(z.string().transform(parseDep)) 31 | .transform((v) => Object.fromEntries(v.filter((d) => !!d))) 32 | .nullish(), 33 | }), 34 | releases: z.record(z.array(PypiRelease).transform((v) => v[0])), 35 | }); 36 | 37 | export type PypiJson = z.infer; 38 | -------------------------------------------------------------------------------- /test/ruby/test/b/Project/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - CPDAcknowledgements (0.5.0) 3 | - Expecta (1.0.5) 4 | - FLKAutoLayout (0.2.1) 5 | - IRFEmojiCheatSheet (0.3.0) 6 | - OCHamcrest (4.3.0) 7 | - OCMockito (1.4.0): 8 | - OCHamcrest (~> 4.0) 9 | - ORStackView (3.0.1): 10 | - FLKAutoLayout (~> 0.2) 11 | - Specta (1.0.5) 12 | 13 | DEPENDENCIES: 14 | - CPDAcknowledgements (from `../CPDAcknowledgements.podspec`) 15 | - Expecta (~> 1.0) 16 | - IRFEmojiCheatSheet 17 | - OCMockito (~> 1.0) 18 | - ORStackView 19 | - Specta (~> 1.0) 20 | 21 | EXTERNAL SOURCES: 22 | CPDAcknowledgements: 23 | :path: "../CPDAcknowledgements.podspec" 24 | 25 | SPEC CHECKSUMS: 26 | CPDAcknowledgements: 32f6dfcc35eed5d9897f13947024b9d32133e2fa 27 | Expecta: e1c022fcd33910b6be89c291d2775b3fe27a89fe 28 | FLKAutoLayout: 9db6b30c2008d230da608e62c607b11c23b942e6 29 | IRFEmojiCheatSheet: 364b2733c4e37c65ef9f56225246cdf42068370f 30 | OCHamcrest: cd63d27f48a266d4412c0b295b01b8f0940efa81 31 | OCMockito: 4981140c9a9ec06c31af40f636e3c0f25f27e6b2 32 | ORStackView: a1bb52748cd0ae29891c140baf22ff8972fb363c 33 | Specta: ac94d110b865115fe60ff2c6d7281053c6f8e8a2 34 | 35 | PODFILE CHECKSUM: 1c110034e6fc846de3ca560c07d3c7cb43780d13 36 | 37 | COCOAPODS: 1.0.0.beta.6 38 | -------------------------------------------------------------------------------- /src/cli/tools/java/resolver.ts: -------------------------------------------------------------------------------- 1 | import { isNonEmptyStringAndNotWhitespace } from '@sindresorhus/is'; 2 | import { injectFromHierarchy, injectable } from 'inversify'; 3 | import { ToolVersionResolver } from '../../install-tool/tool-version-resolver'; 4 | import { resolveLatestJavaLtsVersion } from './utils'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class JavaVersionResolver extends ToolVersionResolver { 9 | readonly tool: string = 'java'; 10 | 11 | async resolve(version: string | undefined): Promise { 12 | if (!isNonEmptyStringAndNotWhitespace(version) || version === 'latest') { 13 | // we know that the latest version is the first entry, so search for first lts 14 | return await resolveLatestJavaLtsVersion( 15 | this.http, 16 | this.tool === 'java-jre' ? 'jre' : 'jdk', 17 | this.env.arch, 18 | ); 19 | } 20 | return version; 21 | } 22 | } 23 | 24 | @injectable() 25 | @injectFromHierarchy() 26 | export class JavaJreVersionResolver extends JavaVersionResolver { 27 | override readonly tool = 'java-jre'; 28 | } 29 | 30 | @injectable() 31 | @injectFromHierarchy() 32 | export class JavaJdkVersionResolver extends JavaVersionResolver { 33 | override readonly tool = 'java-jdk'; 34 | } 35 | -------------------------------------------------------------------------------- /test/bash/util.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Will overwrite certain util functions to make them testable 4 | 5 | # set directories for test 6 | export REPO_DIR="${TEST_DIR}/../.." 7 | export CONTAINERBASE_DIR="${REPO_DIR}/src/usr/local/containerbase" 8 | export ROOT_DIR="${TEST_ROOT_DIR}/root" 9 | export BIN_DIR="${TEST_ROOT_DIR}/bin" 10 | export LIB_DIR="${TEST_ROOT_DIR}/lib" 11 | export USER_HOME="${TEST_ROOT_DIR}/user" 12 | export ENV_FILE="${TEST_ROOT_DIR}/env" 13 | export CONTAINERBASE_VAR_DIR="${TEST_ROOT_DIR}/var" 14 | export CONTAINERBASE_TMP_DIR="${TEST_ROOT_DIR}/tmp" 15 | 16 | # set default test user 17 | export TEST_ROOT_USER=12021 18 | 19 | # Overwrite is_root function to check a test root user 20 | # instead of the effective caller 21 | function is_root () { 22 | if [[ $TEST_ROOT_USER -ne 0 ]]; then 23 | echo 1 24 | else 25 | echo 0 26 | fi 27 | } 28 | 29 | function link_cli_tool () { 30 | local arch=amd64 31 | 32 | if [[ "${ARCHITECTURE}" = "aarch64" ]];then 33 | arch=arm64 34 | fi 35 | export PATH="${TEST_ROOT_DIR}/sbin:${PATH}" 36 | ln -sf "${REPO_DIR}/dist/cli/containerbase-cli-${arch}" "${TEST_ROOT_DIR}/sbin/containerbase-cli" 37 | } 38 | 39 | # ensure directories exist 40 | mkdir -p "${TEST_ROOT_DIR}"/{sbin,root,user} 41 | link_cli_tool 42 | -------------------------------------------------------------------------------- /src/cli/command/uninstall-gem.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest'; 2 | import { testCli } from '../../../test/di'; 3 | import { MissingVersion } from '../utils/codes'; 4 | 5 | const mocks = vi.hoisted(() => ({ 6 | uninstallTool: vi.fn(), 7 | })); 8 | 9 | vi.mock('../install-tool', () => mocks); 10 | 11 | describe('cli/command/uninstall-gem', () => { 12 | test.each([ 13 | { 14 | mode: 'uninstall-gem' as const, 15 | args: [], 16 | }, 17 | { 18 | mode: 'containerbase-cli' as const, 19 | args: ['uninstall', 'gem'], 20 | }, 21 | ])('$mode $args', async ({ mode, args }) => { 22 | const cli = testCli(mode); 23 | expect(await cli.run([...(args ?? []), 'rake'])).toBe(MissingVersion); 24 | 25 | expect(await cli.run([...(args ?? []), 'rake', '5.0.0'])).toBe(0); 26 | 27 | expect(mocks.uninstallTool).toHaveBeenCalledExactlyOnceWith({ 28 | dryRun: false, 29 | recursive: false, 30 | tool: 'rake', 31 | type: 'gem', 32 | version: '5.0.0', 33 | }); 34 | expect(await cli.run([...(args ?? []), 'rake', '-d', '5.0.0'])).toBe(0); 35 | 36 | mocks.uninstallTool.mockRejectedValueOnce(new Error('test')); 37 | expect(await cli.run([...(args ?? []), 'rake', '5.0.0'])).toBe(1); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/cli/command/uninstall-pip.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest'; 2 | import { testCli } from '../../../test/di'; 3 | import { MissingVersion } from '../utils/codes'; 4 | 5 | const mocks = vi.hoisted(() => ({ 6 | uninstallTool: vi.fn(), 7 | })); 8 | 9 | vi.mock('../install-tool', () => mocks); 10 | 11 | describe('cli/command/uninstall-pip', () => { 12 | test.each([ 13 | { 14 | mode: 'uninstall-pip' as const, 15 | args: [], 16 | }, 17 | { 18 | mode: 'containerbase-cli' as const, 19 | args: ['uninstall', 'pip'], 20 | }, 21 | ])('$mode $args', async ({ mode, args }) => { 22 | const cli = testCli(mode); 23 | expect(await cli.run([...(args ?? []), 'poetry'])).toBe(MissingVersion); 24 | 25 | expect(await cli.run([...(args ?? []), 'poetry', '5.0.0'])).toBe(0); 26 | 27 | expect(mocks.uninstallTool).toHaveBeenCalledExactlyOnceWith({ 28 | dryRun: false, 29 | recursive: false, 30 | tool: 'poetry', 31 | type: 'pip', 32 | version: '5.0.0', 33 | }); 34 | expect(await cli.run([...(args ?? []), 'poetry', '-d', '5.0.0'])).toBe(0); 35 | 36 | mocks.uninstallTool.mockRejectedValueOnce(new Error('test')); 37 | expect(await cli.run([...(args ?? []), 'poetry', '5.0.0'])).toBe(1); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/cli/command/uninstall-npm.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest'; 2 | import { testCli } from '../../../test/di'; 3 | import { MissingVersion } from '../utils/codes'; 4 | 5 | const mocks = vi.hoisted(() => ({ 6 | uninstallTool: vi.fn(), 7 | })); 8 | 9 | vi.mock('../install-tool', () => mocks); 10 | 11 | describe('cli/command/uninstall-npm', () => { 12 | test.each([ 13 | { 14 | mode: 'uninstall-npm' as const, 15 | args: [], 16 | }, 17 | { 18 | mode: 'containerbase-cli' as const, 19 | args: ['uninstall', 'npm'], 20 | }, 21 | ])('$mode $args', async ({ mode, args }) => { 22 | const cli = testCli(mode); 23 | expect(await cli.run([...(args ?? []), 'del-cli'])).toBe(MissingVersion); 24 | 25 | expect(await cli.run([...(args ?? []), 'del-cli', '5.0.0'])).toBe(0); 26 | 27 | expect(mocks.uninstallTool).toHaveBeenCalledExactlyOnceWith({ 28 | dryRun: false, 29 | recursive: false, 30 | tool: 'del-cli', 31 | type: 'npm', 32 | version: '5.0.0', 33 | }); 34 | expect(await cli.run([...(args ?? []), 'del-cli', '-d', '5.0.0'])).toBe(0); 35 | 36 | mocks.uninstallTool.mockRejectedValueOnce(new Error('test')); 37 | expect(await cli.run([...(args ?? []), 'del-cli', '5.0.0'])).toBe(1); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/cli/tools/python/poetry.ts: -------------------------------------------------------------------------------- 1 | import { maxSatisfying } from '@renovatebot/pep440'; 2 | import { injectFromHierarchy, injectable } from 'inversify'; 3 | import { logger } from '../../utils'; 4 | import { PipVersionResolver } from './pip'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class PoetryVersionResolver extends PipVersionResolver { 9 | override tool = 'poetry'; 10 | 11 | override async resolve( 12 | version: string | undefined, 13 | ): Promise { 14 | if (version === undefined || version === 'latest') { 15 | const mirrorMeta = await this.fetchMeta('poetry-plugin-pypi-mirror'); 16 | logger.debug({ info: mirrorMeta.info }, 'poetry-plugin-pypi-mirror'); 17 | 18 | const poetryVersion = mirrorMeta.info.requires_dist?.poetry; 19 | 20 | if (!poetryVersion) { 21 | throw new Error('poetry-plugin-pypi-mirror has missing poetry version'); 22 | } 23 | 24 | const meta = await this.fetchMeta(this.tool); 25 | const version = maxSatisfying( 26 | Object.keys(meta.releases).filter((v) => !meta.releases[v]!.yanked), 27 | poetryVersion, 28 | ); 29 | logger.debug({ version }, 'Resolved poetry version'); 30 | return version ?? meta.info.version; 31 | } 32 | return version; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/cdn.md: -------------------------------------------------------------------------------- 1 | # Containerbase CDN 2 | 3 | We provide a CDN for all tools. 4 | This is an experimental feature and might be changed at any time. 5 | It can be combined with [url replacements](./custom-registries.md). 6 | The replacements are applied after the CDN is resolved. 7 | 8 | ## Usage 9 | 10 | Set the `CONTAINERBASE_CDN` environment variable to the CDN URL before calling any containerbase tools. 11 | 12 | All urls will be resolved to the CDN URL. 13 | 14 | ## Additional configuration 15 | 16 | The package managers `gem`, `npm`, `pip` need to be configured to use the CDN explicitly via the following environment variables: 17 | 18 | - `CONTAINERBASE_CDN_GEM=true` 19 | - `CONTAINERBASE_CDN_NPM=true` 20 | - `CONTAINERBASE_CDN_PIP=true` 21 | 22 | ## Sample 23 | 24 | With the following sample the `java` tool will be installed from the CDN. 25 | 26 | ```bash 27 | export CONTAINERBASE_CDN=https://cdn.example.test 28 | install-tool java 29 | ``` 30 | 31 | The following urls will be called: 32 | 33 | - `https://cdn.example.test/api.adoptium.net/v3/info/release_versions?...` (fetch latest Java LTS) 34 | - `https://cdn.example.test/api.adoptium.net/v3/assets/version/{version}?...` (resolve download url) 35 | - `https://cdn.example.test/github.com/adoptium/temurin{major}-binaries/releases/...` (download the binary) 36 | -------------------------------------------------------------------------------- /test/dart/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | #-------------------------------------- 23 | # Image: base 24 | #-------------------------------------- 25 | FROM ${BASE_IMAGE} AS base 26 | 27 | RUN uname -p | tee | grep aarch64 28 | RUN touch /.dummy 29 | 30 | ARG APT_HTTP_PROXY 31 | ARG CONTAINERBASE_CDN 32 | ARG CONTAINERBASE_DEBUG 33 | ARG CONTAINERBASE_LOG_LEVEL 34 | 35 | #-------------------------------------- 36 | # Image: test 37 | #-------------------------------------- 38 | FROM base AS test-dart 39 | 40 | # renovate: datasource=docker 41 | RUN install-tool dart 3.10.7 42 | 43 | #-------------------------------------- 44 | # Image: final 45 | #-------------------------------------- 46 | FROM base 47 | 48 | COPY --from=test-dart /.dummy /.dummy 49 | -------------------------------------------------------------------------------- /test/rust/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | #-------------------------------------- 23 | # Image: base 24 | #-------------------------------------- 25 | FROM ${BASE_IMAGE} AS base 26 | 27 | RUN uname -p | tee | grep aarch64 28 | RUN touch /.dummy 29 | 30 | ARG APT_HTTP_PROXY 31 | ARG CONTAINERBASE_CDN 32 | ARG CONTAINERBASE_DEBUG 33 | ARG CONTAINERBASE_LOG_LEVEL 34 | 35 | #-------------------------------------- 36 | # Image: rust 37 | #-------------------------------------- 38 | FROM base AS test-rust 39 | 40 | # renovate: datasource=docker versioning=docker 41 | RUN install-tool rust 1.92.0 42 | 43 | #-------------------------------------- 44 | # Image: final 45 | #-------------------------------------- 46 | FROM base 47 | 48 | COPY --from=test-rust /.dummy /.dummy 49 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/tools/v2/git-lfs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function check_tool_requirements () { 4 | check_semver "$TOOL_VERSION" "all" 5 | } 6 | 7 | function install_tool () { 8 | local versioned_tool_path 9 | local file 10 | local arch=linux-amd64 11 | local lfs_file 12 | local strip=0 13 | 14 | if [[ "$(uname -p)" = "aarch64" ]]; then 15 | arch=linux-arm64 16 | fi 17 | 18 | lfs_file="${TOOL_NAME}-${arch}-v${TOOL_VERSION}.tar.gz" 19 | file=$(get_from_url "https://github.com/${TOOL_NAME}/${TOOL_NAME}/releases/download/v${TOOL_VERSION}/${lfs_file}") 20 | 21 | # v3.2+ has a subdir https://github.com/git-lfs/git-lfs/pull/4980 22 | if [[ ${MAJOR} -gt 3 || (${MAJOR} -eq 3 && ${MINOR} -ge 2) ]]; then 23 | strip=1 24 | fi 25 | 26 | temp_dir="$(mktemp -d)" 27 | bsdtar --strip $strip -C "${temp_dir}" -xf "${file}" 28 | 29 | versioned_tool_path=$(create_versioned_tool_path) 30 | mkdir "${versioned_tool_path}/bin" 31 | mv "${temp_dir}/git-lfs" "${versioned_tool_path}/bin/" 32 | rm -rf "${temp_dir}" 33 | } 34 | 35 | function link_tool () { 36 | shell_wrapper "${TOOL_NAME}" "$(find_versioned_tool_path)/bin" 37 | 38 | if [ "$(is_root)" -eq 0 ]; then 39 | git lfs install --system 40 | else 41 | git lfs install 42 | fi 43 | } 44 | 45 | test_tool () { 46 | git lfs version 47 | } 48 | -------------------------------------------------------------------------------- /test/swift/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | #-------------------------------------- 23 | # Image: base 24 | #-------------------------------------- 25 | FROM ${BASE_IMAGE} AS base 26 | 27 | RUN uname -p | tee | grep aarch64 28 | RUN touch /.dummy 29 | 30 | ARG APT_HTTP_PROXY 31 | ARG CONTAINERBASE_CDN 32 | ARG CONTAINERBASE_DEBUG 33 | ARG CONTAINERBASE_LOG_LEVEL 34 | 35 | #-------------------------------------- 36 | # Image: swift 37 | #-------------------------------------- 38 | FROM base AS test-swift 39 | 40 | # renovate: datasource=docker versioning=docker 41 | RUN install-tool swift 6.2.3 42 | 43 | #-------------------------------------- 44 | # Image: final 45 | #-------------------------------------- 46 | FROM base 47 | 48 | COPY --from=test-swift /.dummy /.dummy 49 | -------------------------------------------------------------------------------- /src/cli/command/prepare-tool.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cli } from 'clipanion'; 2 | import { describe, expect, test, vi } from 'vitest'; 3 | import { registerCommands } from '.'; 4 | 5 | const mocks = vi.hoisted(() => ({ 6 | installTool: vi.fn(), 7 | prepareTools: vi.fn(), 8 | })); 9 | 10 | vi.mock('../install-tool', () => mocks); 11 | vi.mock('../prepare-tool', () => mocks); 12 | 13 | describe('cli/command/prepare-tool', () => { 14 | test('prepare-tool', async () => { 15 | const cli = new Cli({ binaryName: 'prepare-tool' }); 16 | registerCommands(cli, 'prepare-tool'); 17 | 18 | expect(await cli.run(['node'])).toBe(0); 19 | expect(mocks.prepareTools).toHaveBeenCalledExactlyOnceWith(['node'], false); 20 | 21 | mocks.prepareTools.mockRejectedValueOnce(new Error('test')); 22 | expect(await cli.run(['node'])).toBe(1); 23 | }); 24 | 25 | test('containerbase-cli prepare tool', async () => { 26 | const cli = new Cli({ binaryName: 'containerbase-cli' }); 27 | registerCommands(cli, 'containerbase-cli'); 28 | 29 | expect(await cli.run(['prepare', 'tool', 'node'])).toBe(0); 30 | expect(mocks.prepareTools).toHaveBeenCalledExactlyOnceWith(['node'], false); 31 | 32 | mocks.prepareTools.mockRejectedValueOnce(new Error('test')); 33 | expect(await cli.run(['prepare', 'tool', 'node'])).toBe(1); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/cli/prepare-tool/prepare-legacy-tools.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import spawn from 'nano-spawn'; 3 | import { V2ToolService } from '../services'; 4 | import { logger } from '../utils'; 5 | import { BasePrepareService } from './base-prepare.service'; 6 | 7 | @injectable() 8 | export abstract class V2ToolPrepareService extends BasePrepareService { 9 | @inject(V2ToolService) 10 | private readonly _svc!: V2ToolService; 11 | 12 | override needsInitialize(): boolean { 13 | return this._svc.needsInitialize(this.name); 14 | } 15 | 16 | override needsPrepare(): boolean { 17 | return this._svc.needsPrepare(this.name); 18 | } 19 | 20 | override async initialize(): Promise { 21 | logger.debug(`Initializing v2 tool ${this.name} ...`); 22 | await spawn( 23 | 'bash', 24 | ['/usr/local/containerbase/bin/v2-install-tool.sh', 'init', this.name], 25 | { 26 | stdio: ['inherit', 'inherit', 1], 27 | }, 28 | ); 29 | } 30 | 31 | override async prepare(): Promise { 32 | logger.debug(`Preparing v2 tool ${this.name} ...`); 33 | await spawn( 34 | 'bash', 35 | ['/usr/local/containerbase/bin/v2-install-tool.sh', 'prepare', this.name], 36 | { 37 | stdio: ['inherit', 'inherit', 1], 38 | }, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/flux/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | #-------------------------------------- 23 | # Image: base 24 | #-------------------------------------- 25 | FROM ${BASE_IMAGE} AS base 26 | 27 | RUN uname -p | tee | grep aarch64 28 | RUN touch /.dummy 29 | 30 | ARG APT_HTTP_PROXY 31 | ARG CONTAINERBASE_CDN 32 | ARG CONTAINERBASE_DEBUG 33 | ARG CONTAINERBASE_LOG_LEVEL 34 | 35 | #-------------------------------------- 36 | # Image: flux 37 | #-------------------------------------- 38 | FROM base AS test-flux 39 | 40 | # renovate: datasource=github-releases packageName=fluxcd/flux2 41 | RUN install-tool flux v2.7.5 42 | 43 | #-------------------------------------- 44 | # Image: final 45 | #-------------------------------------- 46 | FROM base 47 | 48 | COPY --from=test-flux /.dummy /.dummy 49 | -------------------------------------------------------------------------------- /test/helm/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | #-------------------------------------- 23 | # Image: base 24 | #-------------------------------------- 25 | FROM ${BASE_IMAGE} AS base 26 | 27 | RUN uname -p | tee | grep aarch64 28 | RUN touch /.dummy 29 | 30 | ARG APT_HTTP_PROXY 31 | ARG CONTAINERBASE_CDN 32 | ARG CONTAINERBASE_DEBUG 33 | ARG CONTAINERBASE_LOG_LEVEL 34 | 35 | #-------------------------------------- 36 | # Image: helm 37 | #-------------------------------------- 38 | FROM base AS test-helm 39 | 40 | # renovate: datasource=github-releases packageName=helm/helm 41 | RUN install-tool helm v4.0.4 42 | 43 | #-------------------------------------- 44 | # Image: final 45 | #-------------------------------------- 46 | FROM base 47 | 48 | COPY --from=test-helm /.dummy /.dummy 49 | -------------------------------------------------------------------------------- /test/dotnet/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | #-------------------------------------- 23 | # Image: base 24 | #-------------------------------------- 25 | FROM ${BASE_IMAGE} AS base 26 | 27 | RUN uname -p | tee | grep aarch64 28 | RUN touch /.dummy 29 | 30 | ARG APT_HTTP_PROXY 31 | ARG CONTAINERBASE_CDN 32 | ARG CONTAINERBASE_DEBUG 33 | ARG CONTAINERBASE_LOG_LEVEL 34 | 35 | #-------------------------------------- 36 | # Image: dotnet 37 | #-------------------------------------- 38 | FROM base AS test-dotnet 39 | 40 | # renovate: datasource=dotnet packageName=dotnet-sdk 41 | RUN install-tool dotnet 10.0.101 42 | 43 | #-------------------------------------- 44 | # Image: final 45 | #-------------------------------------- 46 | FROM base 47 | 48 | COPY --from=test-dotnet /.dummy /.dummy 49 | -------------------------------------------------------------------------------- /test/jb/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | #-------------------------------------- 23 | # Image: base 24 | #-------------------------------------- 25 | FROM ${BASE_IMAGE} AS base 26 | 27 | RUN uname -p | tee | grep aarch64 28 | RUN touch /.dummy 29 | 30 | ARG APT_HTTP_PROXY 31 | ARG CONTAINERBASE_CDN 32 | ARG CONTAINERBASE_DEBUG 33 | ARG CONTAINERBASE_LOG_LEVEL 34 | 35 | #-------------------------------------- 36 | # Image: jb 37 | #-------------------------------------- 38 | FROM base AS test-jb 39 | 40 | # renovate: datasource=github-releases packageName=jsonnet-bundler/jsonnet-bundler 41 | RUN install-tool jb v0.6.0 42 | 43 | #-------------------------------------- 44 | # Image: final 45 | #-------------------------------------- 46 | FROM base 47 | 48 | COPY --from=test-jb /.dummy /.dummy 49 | -------------------------------------------------------------------------------- /test/nix/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | #-------------------------------------- 23 | # Image: base 24 | #-------------------------------------- 25 | FROM ${BASE_IMAGE} AS base 26 | 27 | RUN uname -p | tee | grep aarch64 28 | RUN touch /.dummy 29 | 30 | ARG APT_HTTP_PROXY 31 | ARG CONTAINERBASE_CDN 32 | ARG CONTAINERBASE_DEBUG 33 | ARG CONTAINERBASE_LOG_LEVEL 34 | 35 | #-------------------------------------- 36 | # Image: nix 37 | #-------------------------------------- 38 | FROM base AS test-nix 39 | 40 | # renovate: datasource=github-releases packageName=containerbase/nix-prebuild 41 | RUN install-tool nix 2.33.0 42 | 43 | #-------------------------------------- 44 | # Image: final 45 | #-------------------------------------- 46 | FROM base 47 | 48 | COPY --from=test-nix /.dummy /.dummy 49 | -------------------------------------------------------------------------------- /test/powershell/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | #-------------------------------------- 23 | # Image: base 24 | #-------------------------------------- 25 | FROM ${BASE_IMAGE} AS base 26 | 27 | RUN uname -p | tee | grep aarch64 28 | RUN touch /.dummy 29 | 30 | ARG APT_HTTP_PROXY 31 | ARG CONTAINERBASE_CDN 32 | ARG CONTAINERBASE_DEBUG 33 | ARG CONTAINERBASE_LOG_LEVEL 34 | 35 | #-------------------------------------- 36 | # Image: test 37 | #-------------------------------------- 38 | FROM base AS test-powershell 39 | 40 | # renovate: datasource=github-releases packageName=PowerShell/PowerShell 41 | RUN install-tool powershell v7.5.4 42 | 43 | #-------------------------------------- 44 | # Image: final 45 | #-------------------------------------- 46 | FROM base 47 | 48 | COPY --from=test-powershell /.dummy /.dummy 49 | -------------------------------------------------------------------------------- /src/cli/command/install-npm.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'clipanion'; 2 | import { InstallToolCommand } from './install-tool'; 3 | import { command } from './utils'; 4 | 5 | @command('containerbase-cli') 6 | export class InstallNpmCommand extends InstallToolCommand { 7 | static override paths = [['install', 'npm']]; 8 | static override usage = Command.Usage({ 9 | description: 'Installs a npm package into the container.', 10 | examples: [ 11 | ['Installs del-cli 5.0.0', '$0 install npm del-cli 5.0.0'], 12 | [ 13 | 'Installs del-cli with version via environment variable', 14 | 'DEL_CLI_VERSION=5.0.0 $0 install npm del-cli', 15 | ], 16 | ['Installs latest del-cli version', '$0 install npm del-cli'], 17 | ], 18 | }); 19 | 20 | protected override type = 'npm' as const; 21 | } 22 | 23 | @command('install-npm') 24 | export class InstallNpmShortCommand extends InstallNpmCommand { 25 | static override paths = [Command.Default]; 26 | 27 | static override usage = Command.Usage({ 28 | description: 'Installs a npm package into the container.', 29 | examples: [ 30 | ['Installs del-cli v5.0.0', '$0 del-cli 5.0.0'], 31 | [ 32 | 'Installs del-cli with version via environment variable', 33 | 'DEL_CLI_VERSION=5.0.0 $0 del-cli', 34 | ], 35 | ['Installs latest del-cli version', '$0 del-cli'], 36 | ], 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /tools/prepare-release.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import { Command, Option, runExit } from 'clipanion'; 3 | import shell from 'shelljs'; 4 | import { hashFile } from './utils.js'; 5 | 6 | shell.config.fatal = true; 7 | 8 | class PrepareCommand extends Command { 9 | release = Option.String('-r,--release', { required: true }); 10 | gitSha = Option.String('--sha'); 11 | dryRun = Option.Boolean('-d,--dry-run'); 12 | 13 | async execute() { 14 | const version = this.release; 15 | 16 | shell.echo(`Preparing version: ${version}`); 17 | 18 | if (this.dryRun) { 19 | shell.echo('DRY-RUN: done.'); 20 | return 0; 21 | } 22 | 23 | process.env.TAG = version; 24 | process.env.CONTAINERBASE_VERSION = version; 25 | 26 | shell.mkdir('-p', 'bin'); 27 | 28 | shell.exec('pnpm build'); 29 | 30 | await fs.writeFile('dist/docker/usr/local/containerbase/version', version); 31 | shell.exec(`tar -cJf ./bin/containerbase.tar.xz -C ./dist/docker .`); 32 | 33 | await hashFile('./bin/containerbase.tar.xz', 'sha512'); 34 | await hashFile('./dist/cli/containerbase-cli-amd64', 'sha512'); 35 | await hashFile('./dist/cli/containerbase-cli-arm64', 'sha512'); 36 | 37 | shell.exec( 38 | 'docker buildx bake --set settings.platform=linux/amd64,linux/arm64 build', 39 | ); 40 | 41 | return 0; 42 | } 43 | } 44 | 45 | void runExit(PrepareCommand); 46 | -------------------------------------------------------------------------------- /src/cli/command/install-gem.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'clipanion'; 2 | import { InstallToolCommand } from './install-tool'; 3 | import { command } from './utils'; 4 | 5 | @command('containerbase-cli') 6 | export class InstallGemCommand extends InstallToolCommand { 7 | static override paths = [['install', 'gem']]; 8 | static override usage = Command.Usage({ 9 | description: 'Installs a gem package into the container.', 10 | examples: [ 11 | ['Installs rake 13.0.6', '$0 install gem rake 13.0.6'], 12 | [ 13 | 'Installs rake with version via environment variable', 14 | 'RAKE_VERSION=13.0.6 $0 install gem rake', 15 | ], 16 | // ['Installs latest rake version', '$0 install gem rake'], // not yet supported 17 | ], 18 | }); 19 | 20 | protected override type = 'gem' as const; 21 | } 22 | 23 | @command('install-gem') 24 | export class InstallGemShortCommand extends InstallGemCommand { 25 | static override paths = [Command.Default]; 26 | 27 | static override usage = Command.Usage({ 28 | description: 'Installs a gem package into the container.', 29 | examples: [ 30 | ['Installs rake v13.0.6', '$0 rake 13.0.6'], 31 | [ 32 | 'Installs rake with version via environment variable', 33 | 'RAKE_VERSION=13.0.6 $0 rake', 34 | ], 35 | // ['Installs latest rake version', '$0 rake'], // not yet supported 36 | ], 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /test/flutter/test/a/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | characters: 5 | dependency: transitive 6 | description: 7 | name: characters 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "1.1.0-nullsafety.3" 11 | collection: 12 | dependency: "direct main" 13 | description: 14 | name: collection 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.15.0-nullsafety.3" 18 | flutter: 19 | dependency: "direct main" 20 | description: flutter 21 | source: sdk 22 | version: "0.0.0" 23 | meta: 24 | dependency: "direct main" 25 | description: 26 | name: meta 27 | url: "https://pub.dartlang.org" 28 | source: hosted 29 | version: "1.3.0-nullsafety.3" 30 | sky_engine: 31 | dependency: transitive 32 | description: flutter 33 | source: sdk 34 | version: "0.0.99" 35 | typed_data: 36 | dependency: "direct main" 37 | description: 38 | name: typed_data 39 | url: "https://pub.dartlang.org" 40 | source: hosted 41 | version: "1.3.0-nullsafety.3" 42 | vector_math: 43 | dependency: "direct main" 44 | description: 45 | name: vector_math 46 | url: "https://pub.dartlang.org" 47 | source: hosted 48 | version: "2.1.0-nullsafety.3" 49 | sdks: 50 | dart: ">=2.10.0-110 <2.11.0" 51 | -------------------------------------------------------------------------------- /test/golang/test/d/vendor/github.com/pkg/errors/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Dave Cheney 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/tools/v2/powershell.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function prepare_tool() { 4 | local version_codename 5 | 6 | version_codename="$(get_distro)" 7 | case "${version_codename}" in 8 | "focal") apt_install libc6 libgcc1 libgssapi-krb5-2 libicu66 libssl1.1 libstdc++6 zlib1g;; 9 | "jammy") apt_install libc6 libgcc1 libgssapi-krb5-2 libicu70 libssl3 libstdc++6 zlib1g;; 10 | "noble") apt_install libc6 libgcc1 libgssapi-krb5-2 libicu74 libssl3 libstdc++6 zlib1g;; 11 | *) 12 | echo "Tool '${TOOL_NAME}' not supported on: ${version_codename}! Please use ubuntu 'focal' or 'jammy'." >&2 13 | exit 1 14 | ;; 15 | esac 16 | } 17 | 18 | function install_tool () { 19 | local file 20 | local versioned_tool_path 21 | local arch=linux-x64 22 | 23 | if [[ "$(uname -p)" = "aarch64" ]]; then 24 | arch=linux-arm64 25 | fi 26 | 27 | file=$(get_from_url "https://github.com/PowerShell/PowerShell/releases/download/v${TOOL_VERSION}/powershell-${TOOL_VERSION}-${arch}.tar.gz") 28 | 29 | versioned_tool_path=$(create_versioned_tool_path) 30 | bsdtar -C "${versioned_tool_path}" -xzf "${file}" 31 | # Happened on v7.3.0 32 | if [[ ! -x "${versioned_tool_path}/pwsh" ]]; then 33 | chmod +x "${versioned_tool_path}/pwsh" 34 | fi 35 | } 36 | 37 | function link_tool () { 38 | shell_wrapper pwsh "$(find_versioned_tool_path)" 39 | } 40 | 41 | function test_tool () { 42 | pwsh -version 43 | } 44 | -------------------------------------------------------------------------------- /patches/nano-spawn.patch: -------------------------------------------------------------------------------- 1 | diff --git a/source/spawn.js b/source/spawn.js 2 | index 0018a555bed498c5ff530139598158801683cb0b..389f0bc893beb2c40627e2b80ba2a653678bedcf 100644 3 | --- a/source/spawn.js 4 | +++ b/source/spawn.js 5 | @@ -1,20 +1,10 @@ 6 | import {spawn} from 'node:child_process'; 7 | import {once} from 'node:events'; 8 | -import process from 'node:process'; 9 | import {applyForceShell} from './windows.js'; 10 | import {getResultError} from './result.js'; 11 | 12 | export const spawnSubprocess = async (file, commandArguments, options, context) => { 13 | try { 14 | - // When running `node`, keep the current Node version and CLI flags. 15 | - // Not applied with file paths to `.../node` since those indicate a clear intent to use a specific Node version. 16 | - // This also provides a way to opting out, e.g. using `process.execPath` instead of `node` to discard current CLI flags. 17 | - // Does not work with shebangs, but those don't work cross-platform anyway. 18 | - if (['node', 'node.exe'].includes(file.toLowerCase())) { 19 | - file = process.execPath; 20 | - commandArguments = [...process.execArgv.filter(flag => !flag.startsWith('--inspect')), ...commandArguments]; 21 | - } 22 | - 23 | [file, commandArguments, options] = await applyForceShell(file, commandArguments, options); 24 | [file, commandArguments, options] = concatenateShell(file, commandArguments, options); 25 | const instance = spawn(file, commandArguments, options); 26 | -------------------------------------------------------------------------------- /src/cli/command/file-exists.spec.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'node:process'; 2 | import { Cli } from 'clipanion'; 3 | import { beforeEach, describe, expect, test, vi } from 'vitest'; 4 | import { registerCommands } from '.'; 5 | import { scope } from '~test/http-mock'; 6 | 7 | const mocks = vi.hoisted(() => ({ 8 | installTool: vi.fn(), 9 | prepareTools: vi.fn(), 10 | })); 11 | 12 | vi.mock('../install-tool', () => mocks); 13 | vi.mock('../prepare-tool', () => mocks); 14 | 15 | describe('cli/command/file-exists', () => { 16 | beforeEach(() => { 17 | for (const key of Object.keys(env)) { 18 | if (key.startsWith('URL_REPLACE_')) { 19 | delete env[key]; 20 | } 21 | } 22 | }); 23 | 24 | test('file-exists', async () => { 25 | const cli = new Cli({ binaryName: 'containerbase-cli' }); 26 | registerCommands(cli, null); 27 | 28 | const baseUrl = 'https://example.com'; 29 | scope(baseUrl) 30 | .head('/file.txt') 31 | .reply(200, 'ok') 32 | .head('/fail.txt') 33 | .reply(404); 34 | 35 | env.URL_REPLACE_0_FROM = 'https://example.test'; 36 | env.URL_REPLACE_0_TO = baseUrl; 37 | 38 | expect( 39 | await cli.run(['file', 'exists', 'https://example.test/file.txt']), 40 | ).toBe(0); 41 | 42 | expect( 43 | await cli.run(['file', 'exists', 'https://example.test/fail.txt']), 44 | ).toBe(1); 45 | 46 | expect(await cli.run(['file', 'exists', ''])).toBe(-1); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/global-setup.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { afterAll, beforeAll, vi } from 'vitest'; 3 | 4 | vi.mock('pino', () => ({ 5 | default: vi.fn(() => ({ 6 | debug: vi.fn(), 7 | error: vi.fn(), 8 | fatal: vi.fn(), 9 | info: vi.fn(), 10 | trace: vi.fn(), 11 | warn: vi.fn(), 12 | })), 13 | transport: vi.fn(), 14 | levels: { values: {} }, 15 | })); 16 | 17 | let rootDir!: string; 18 | let cacheDir!: string; 19 | 20 | beforeAll(async () => { 21 | const fs = 22 | await vi.importActual( 23 | 'node:fs/promises', 24 | ); 25 | const os = await vi.importActual('node:os'); 26 | const path = await vi.importActual('node:path'); 27 | cacheDir = globalThis.cacheDir = await fs.mkdtemp( 28 | path.join(os.tmpdir(), 'containerbase-cache-'), 29 | ); 30 | rootDir = globalThis.rootDir = await fs.mkdtemp( 31 | path.join(os.tmpdir(), 'containerbase-root-'), 32 | ); 33 | 34 | const { env } = 35 | await vi.importActual('node:process'); 36 | 37 | env.CONTAINERBASE_CACHE_DIR = globalThis.cacheDir; 38 | }); 39 | 40 | afterAll(async () => { 41 | const fs = 42 | await vi.importActual( 43 | 'node:fs/promises', 44 | ); 45 | await fs.rm(cacheDir, { recursive: true, force: true }); 46 | await fs.rm(rootDir, { recursive: true, force: true }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/bash/v2/defaults.bats: -------------------------------------------------------------------------------- 1 | # shellcheck disable=SC2034,SC2148 2 | 3 | setup() { 4 | load "$BATS_SUPPORT_LOAD_PATH" 5 | load "$BATS_ASSERT_LOAD_PATH" 6 | 7 | TEST_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")" >/dev/null 2>&1 && pwd)" 8 | TEST_ROOT_DIR=$(mktemp -u) 9 | 10 | load "$TEST_DIR/../../../src/usr/local/containerbase/util.sh" 11 | 12 | # load v2 overwrites 13 | load "$TEST_DIR/../../../src/usr/local/containerbase/utils/v2/overrides.sh" 14 | 15 | # load test overwrites 16 | load "$TEST_DIR/../util.sh" 17 | } 18 | 19 | teardown() { 20 | rm -rf "${TEST_ROOT_DIR}" 21 | } 22 | 23 | @test "overwrite: test default functions" { 24 | 25 | run check_tool_requirements 26 | assert_failure 27 | assert_output --partial "Not a semver like version" 28 | 29 | TOOL_VERSION=1.2.3 30 | run check_tool_requirements 31 | assert_success 32 | 33 | run check_tool_installed 34 | assert_failure 35 | 36 | TOOL_NAME=foo \ 37 | TOOL_VERSION=1.2.3 38 | run check_tool_installed 39 | assert_failure 40 | 41 | TOOL_NAME=foo \ 42 | TOOL_VERSION=1.2.3 43 | run create_versioned_tool_path 44 | assert_success 45 | 46 | TOOL_NAME=foo \ 47 | TOOL_VERSION=1.2.3 48 | run check_tool_installed 49 | assert_success 50 | 51 | run install_tool 52 | assert_failure 53 | assert_output --partial "not defined" 54 | 55 | run link_tool 56 | assert_failure 57 | assert_output --partial "not defined" 58 | 59 | run prepare_tool 60 | assert_success 61 | } 62 | -------------------------------------------------------------------------------- /src/cli/command/install-pip.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'clipanion'; 2 | import { InstallToolCommand } from './install-tool'; 3 | import { command } from './utils'; 4 | 5 | @command('containerbase-cli') 6 | export class InstallPipCommand extends InstallToolCommand { 7 | static override paths = [['install', 'pip']]; 8 | static override usage = Command.Usage({ 9 | description: 'Installs a pip package into the container.', 10 | examples: [ 11 | ['Installs checkov 2.4.7', '$0 install pip checkov 2.4.7'], 12 | [ 13 | 'Installs checkov with version via environment variable', 14 | 'DEL_CLI_VERSION=2.4.7 $0 install pip checkov', 15 | ], 16 | // TODO: add version resolver 17 | // ['Installs latest checkov version', '$0 install pip checkov'], 18 | ], 19 | }); 20 | 21 | protected override type = 'pip' as const; 22 | } 23 | 24 | @command('install-pip') 25 | export class InstallPipShortCommand extends InstallPipCommand { 26 | static override paths = [Command.Default]; 27 | 28 | static override usage = Command.Usage({ 29 | description: 'Installs a pip package into the container.', 30 | examples: [ 31 | ['Installs checkov v5.0.0', '$0 checkov 2.4.7'], 32 | [ 33 | 'Installs checkov with version via environment variable', 34 | 'DEL_CLI_VERSION=2.4.7 $0 checkov', 35 | ], 36 | // TODO: add version resolver 37 | // ['Installs latest checkov version', '$0 checkov'], 38 | ], 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/flutter/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | # flutter currently needs git 23 | # renovate: datasource=github-tags packageName=git/git 24 | RUN install-tool git v2.52.0 25 | 26 | #-------------------------------------- 27 | # Image: base 28 | #-------------------------------------- 29 | FROM ${BASE_IMAGE} AS base 30 | 31 | RUN uname -p | tee | grep aarch64 32 | RUN touch /.dummy 33 | 34 | ARG APT_HTTP_PROXY 35 | ARG CONTAINERBASE_CDN 36 | ARG CONTAINERBASE_DEBUG 37 | ARG CONTAINERBASE_LOG_LEVEL 38 | 39 | #-------------------------------------- 40 | # Image: flutter 41 | #-------------------------------------- 42 | FROM base AS test-flutter 43 | 44 | # renovate: datasource=github-releases packageName=containerbase/flutter-prebuild 45 | RUN install-tool flutter 3.38.5 46 | 47 | #-------------------------------------- 48 | # Image: final 49 | #-------------------------------------- 50 | FROM base 51 | 52 | COPY --from=test-flutter /.dummy /.dummy 53 | -------------------------------------------------------------------------------- /test/golang/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | # currently go doesn't install git 23 | # renovate: datasource=github-tags packageName=git/git 24 | RUN install-tool git v2.52.0 25 | 26 | #-------------------------------------- 27 | # Image: base 28 | #-------------------------------------- 29 | FROM ${BASE_IMAGE} AS base 30 | 31 | RUN uname -p | tee | grep aarch64 32 | RUN touch /.dummy 33 | 34 | ARG APT_HTTP_PROXY 35 | ARG CONTAINERBASE_CDN 36 | ARG CONTAINERBASE_DEBUG 37 | ARG CONTAINERBASE_LOG_LEVEL 38 | 39 | #-------------------------------------- 40 | # Image: golang 41 | #-------------------------------------- 42 | FROM base AS test-golang 43 | 44 | # renovate: datasource=github-releases packageName=containerbase/golang-prebuild 45 | RUN install-tool golang 1.25.5 46 | 47 | #-------------------------------------- 48 | # Image: final 49 | #-------------------------------------- 50 | FROM base 51 | 52 | COPY --from=test-golang /.dummy /.dummy 53 | -------------------------------------------------------------------------------- /src/cli/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'node:process'; 2 | import { isNonEmptyStringAndNotWhitespace } from '@sindresorhus/is'; 3 | // eslint-disable-next-line import-x/no-named-as-default 4 | import pino, { type TransportTargetOptions, levels, transport } from 'pino'; 5 | 6 | const level = 7 | [ 8 | env.CONTAINERBASE_LOG_LEVEL, 9 | env.CONTAINERBASE_DEBUG ? 'debug' : undefined, 10 | env.LOG_LEVEL, 11 | ] 12 | .filter(isNonEmptyStringAndNotWhitespace) 13 | .shift() ?? 'info'; 14 | 15 | const format = 16 | [env.CONTAINERBASE_LOG_FORMAT, env.LOG_FORMAT] 17 | .filter(isNonEmptyStringAndNotWhitespace) 18 | .shift() 19 | ?.toLowerCase() ?? 'pretty'; 20 | 21 | const stdoutTransportTarget = format === 'json' ? 'pino/file' : 'pino-pretty'; 22 | 23 | let fileLevel = 'silent'; 24 | 25 | const targets: TransportTargetOptions[] = [ 26 | { target: stdoutTransportTarget, level, options: {} }, 27 | ]; 28 | 29 | if (isNonEmptyStringAndNotWhitespace(env.CONTAINERBASE_LOG_FILE)) { 30 | fileLevel = env.CONTAINERBASE_LOG_FILE_LEVEL ?? 'debug'; 31 | targets.push({ 32 | target: 'pino/file', 33 | level: fileLevel, 34 | options: { 35 | destination: env.CONTAINERBASE_LOG_FILE, 36 | }, 37 | }); 38 | } 39 | 40 | const transports = transport({ 41 | targets, 42 | }); 43 | 44 | const numLevel = levels.values[level] ?? Infinity; 45 | const numFileLevel = levels.values[fileLevel] ?? Infinity; 46 | export const logger = pino( 47 | { level: numLevel < numFileLevel ? level : fileLevel }, 48 | transports, 49 | ); 50 | -------------------------------------------------------------------------------- /src/cli/prepare-tool/index.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { beforeAll, describe, expect, test, vi } from 'vitest'; 3 | import { PathService, createContainer } from '../services'; 4 | import { initializeTools, prepareTools } from '.'; 5 | import { ensurePaths, rootPath } from '~test/path'; 6 | 7 | vi.mock('del'); 8 | vi.mock('nano-spawn'); 9 | vi.mock('../tools/bun'); 10 | vi.mock('../tools/php/composer'); 11 | 12 | vi.mock('node:process', async (importOriginal) => ({ 13 | ...(await importOriginal()), 14 | geteuid: () => 0, 15 | })); 16 | 17 | describe('cli/prepare-tool/index', () => { 18 | beforeAll(async () => { 19 | await ensurePaths([ 20 | 'tmp/containerbase/tool.init.d', 21 | 'usr/local/containerbase/tools/v2', 22 | 'var/lib/containerbase/tool.prep.d', 23 | ]); 24 | 25 | await fs.writeFile( 26 | rootPath('usr/local/containerbase/tools/v2/dummy.sh'), 27 | '', 28 | ); 29 | 30 | const child = createContainer(); 31 | const pathSvc = await child.getAsync(PathService); 32 | await pathSvc.setPrepared('bun'); 33 | }); 34 | 35 | test('prepareTools', async () => { 36 | expect(await prepareTools(['bun', 'dummy'])).toBeUndefined(); 37 | expect(await prepareTools(['not-exist'])).toBe(1); 38 | }); 39 | 40 | test('initializeTools', async () => { 41 | expect(await initializeTools(['bun', 'dummy'])).toBeUndefined(); 42 | expect(await initializeTools(['not-exist'])).toBeUndefined(); 43 | expect(await initializeTools(['all'])).toBeUndefined(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/tools/v2/nix.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function install_tool() { 4 | local arch 5 | local checksum_file 6 | local expected_checksum 7 | local file 8 | local name=${TOOL_NAME} 9 | local tool_path 10 | local version=${TOOL_VERSION} 11 | local versioned_tool_path 12 | 13 | if [[ ${MAJOR} -lt 2 || (${MAJOR} -eq 2 && ${MINOR} -lt 10) || (${MAJOR} -eq 2 && ${MINOR} -eq 10 && ${PATCH} -lt 3) ]]; then 14 | echo "Nix version ${version} is not supported! Use v2.10.3 or higher." >&2 15 | exit 1 16 | fi 17 | 18 | arch=$(uname -m) 19 | base_url="https://github.com/containerbase/${name}-prebuild/releases/download" 20 | 21 | tool_path=$(create_tool_path) 22 | checksum_file=$(get_from_url "${base_url}/${version}/${name}-${version}-${arch}.tar.xz.sha512") 23 | expected_checksum=$(cat "${checksum_file}") 24 | file=$(get_from_url \ 25 | "${base_url}/${version}/${name}-${version}-${arch}.tar.xz" \ 26 | "${name}-${version}-${arch}.tar.xz" \ 27 | "${expected_checksum}" \ 28 | sha512sum 29 | ) 30 | 31 | bsdtar -C "${tool_path}" -xf "${file}" 32 | } 33 | 34 | function link_tool() { 35 | local versioned_tool_path 36 | versioned_tool_path=$(find_versioned_tool_path) 37 | 38 | shell_wrapper "${TOOL_NAME}" "${versioned_tool_path}/bin" "NIX_STORE_DIR=$(get_cache_path)/nix/store NIX_DATA_DIR=$(get_cache_path)/nix/data NIX_LOG_DIR=$(get_cache_path)/nix/log NIX_STATE_DIR=$(get_cache_path)/nix/state NIX_CONF_DIR=$(get_cache_path)/nix/conf" 39 | } 40 | 41 | function test_tool () { 42 | nix --version 43 | } 44 | -------------------------------------------------------------------------------- /patches/global-agent.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/factories/createGlobalProxyAgent.js b/dist/factories/createGlobalProxyAgent.js 2 | index c87b9ed04f1cf8314374d9e169383e3b81904c22..a17fcde1b68706694ecdce5ae97e650065d5da87 100644 3 | --- a/dist/factories/createGlobalProxyAgent.js 4 | +++ b/dist/factories/createGlobalProxyAgent.js 5 | @@ -11,7 +11,7 @@ var _https = _interopRequireDefault(require("https")); 6 | 7 | var _boolean = require("boolean"); 8 | 9 | -var _semver = _interopRequireDefault(require("semver")); 10 | +var _semverGte = require("semver/functions/gte"); 11 | 12 | var _Logger = _interopRequireDefault(require("../Logger")); 13 | 14 | @@ -138,7 +138,7 @@ const createGlobalProxyAgent = (configurationInput = defaultConfigurationInput) 15 | const httpsAgent = new BoundHttpsProxyAgent(); // Overriding globalAgent was added in v11.7. 16 | // @see https://nodejs.org/uk/blog/release/v11.7.0/ 17 | 18 | - if (_semver.default.gte(process.version, 'v11.7.0')) { 19 | + if (_semverGte(process.version, 'v11.7.0')) { 20 | // @see https://github.com/facebook/flow/issues/7670 21 | // $FlowFixMe 22 | _http.default.globalAgent = httpAgent; // $FlowFixMe 23 | @@ -154,7 +154,7 @@ const createGlobalProxyAgent = (configurationInput = defaultConfigurationInput) 24 | // in `bindHttpMethod`. 25 | 26 | 27 | - if (_semver.default.gte(process.version, 'v10.0.0')) { 28 | + if (_semverGte(process.version, 'v10.0.0')) { 29 | // $FlowFixMe 30 | _http.default.get = (0, _utilities.bindHttpMethod)(httpGet, httpAgent, configuration.forceGlobalAgent); // $FlowFixMe 31 | 32 | -------------------------------------------------------------------------------- /src/cli/tools/protoc.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { injectFromHierarchy, injectable } from 'inversify'; 3 | import { BaseInstallService } from '../install-tool/base-install.service'; 4 | import { semverCoerce } from '../utils'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class ProtocInstallService extends BaseInstallService { 9 | readonly name = 'protoc'; 10 | 11 | private get ghArch(): string { 12 | switch (this.envSvc.arch) { 13 | case 'arm64': 14 | return 'aarch_64'; 15 | case 'amd64': 16 | return 'x86_64'; 17 | } 18 | } 19 | 20 | override async install(version: string): Promise { 21 | const name = this.name; 22 | 23 | const url = `https://github.com/protocolbuffers/protobuf/releases/download/v${version}/${name}-${version}-linux-${this.ghArch}.zip`; 24 | 25 | const file = await this.http.download({ 26 | url, 27 | }); 28 | 29 | const cwd = await this.pathSvc.createVersionedToolPath(name, version); 30 | 31 | await this.compress.extract({ file, cwd }); 32 | } 33 | 34 | override async link(version: string): Promise { 35 | const src = path.join( 36 | this.pathSvc.versionedToolPath(this.name, version), 37 | 'bin', 38 | ); 39 | await this.shellwrapper({ srcDir: src }); 40 | } 41 | 42 | override async test(_version: string): Promise { 43 | await this._spawn(this.name, ['--version']); 44 | } 45 | 46 | override validate(version: string): Promise { 47 | return Promise.resolve(semverCoerce(version) !== null); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/dotnet/test/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | ".NETCoreApp,Version=v3.1": { 5 | "Newtonsoft.Json": { 6 | "type": "Direct", 7 | "requested": "[12.0.1, )", 8 | "resolved": "12.0.1", 9 | "contentHash": "pBR3wCgYWZGiaZDYP+HHYnalVnPJlpP1q55qvVb+adrDHmFMDc1NAKio61xTwftK3Pw5h7TZJPJEEVMd6ty8rg==" 10 | } 11 | }, 12 | ".NETStandard,Version=v2.0": { 13 | "NETStandard.Library": { 14 | "type": "Direct", 15 | "requested": "[2.0.3, )", 16 | "resolved": "2.0.3", 17 | "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", 18 | "dependencies": { 19 | "Microsoft.NETCore.Platforms": "1.1.0" 20 | } 21 | }, 22 | "Newtonsoft.Json": { 23 | "type": "Direct", 24 | "requested": "[12.0.1, )", 25 | "resolved": "12.0.1", 26 | "contentHash": "pBR3wCgYWZGiaZDYP+HHYnalVnPJlpP1q55qvVb+adrDHmFMDc1NAKio61xTwftK3Pw5h7TZJPJEEVMd6ty8rg==" 27 | }, 28 | "Microsoft.NETCore.Platforms": { 29 | "type": "Transitive", 30 | "resolved": "1.1.0", 31 | "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" 32 | } 33 | }, 34 | "net6.0": { 35 | "Newtonsoft.Json": { 36 | "type": "Direct", 37 | "requested": "[12.0.1, )", 38 | "resolved": "12.0.1", 39 | "contentHash": "pBR3wCgYWZGiaZDYP+HHYnalVnPJlpP1q55qvVb+adrDHmFMDc1NAKio61xTwftK3Pw5h7TZJPJEEVMd6ty8rg==" 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/cli/tools/bazelisk.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | import { injectFromHierarchy, injectable } from 'inversify'; 4 | import { BaseInstallService } from '../install-tool/base-install.service'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class BazeliskInstallService extends BaseInstallService { 9 | readonly name = 'bazelisk'; 10 | 11 | override async install(version: string): Promise { 12 | const baseurl = `https://github.com/bazelbuild/bazelisk/releases/download/v${version}/`; 13 | const filename = `bazelisk-linux-${this.envSvc.arch}`; 14 | 15 | const file = await this.http.download({ 16 | url: `${baseurl}${filename}`, 17 | }); 18 | 19 | await this.pathSvc.ensureToolPath(this.name); 20 | 21 | const path = join( 22 | await this.pathSvc.createVersionedToolPath(this.name, version), 23 | 'bin', 24 | ); 25 | await fs.mkdir(path); 26 | 27 | const binarypath = join(path, 'bazelisk'); 28 | await fs.copyFile(file, binarypath); 29 | await this.pathSvc.setOwner({ 30 | path: binarypath, 31 | }); 32 | await fs.symlink(binarypath, join(path, 'bazel')); 33 | } 34 | 35 | override async link(version: string): Promise { 36 | const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin'); 37 | 38 | await this.shellwrapper({ 39 | srcDir: src, 40 | }); 41 | await this.shellwrapper({ 42 | name: 'bazel', 43 | srcDir: src, 44 | }); 45 | } 46 | 47 | override async test(_version: string): Promise { 48 | await this._spawn('bazelisk', ['version']); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/tools/v2/sbt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function check_tool_requirements () { 4 | check_command java 5 | check_semver "$TOOL_VERSION" "all" 6 | } 7 | 8 | function prepare_tool() { 9 | init_tool 10 | 11 | # Redirect mix home 12 | path="$(get_cache_path)/.sbt" 13 | ln -sf "${path}" "${USER_HOME}/.sbt" 14 | } 15 | 16 | function init_tool () { 17 | local path 18 | path="$(get_cache_path)/.sbt" 19 | 20 | if [ -d "${path}" ]; then 21 | return 22 | fi 23 | 24 | # Init mix home 25 | create_folder "${path}" 775 26 | chown -R "${USER_ID}" "${path}" 27 | } 28 | 29 | 30 | function install_tool () { 31 | local versioned_tool_path 32 | local file 33 | local URL='https://github.com' 34 | 35 | # https://github.com/sbt/sbt/releases/download/v1.5.2/sbt-1.5.2.tgz 36 | file=$(get_from_url "${URL}/${TOOL_NAME}/${TOOL_NAME}/releases/download/v${TOOL_VERSION}/${TOOL_NAME}-${TOOL_VERSION}.tgz") 37 | 38 | versioned_tool_path=$(create_versioned_tool_path) 39 | tar --strip 1 -C "${versioned_tool_path}" -xf "${file}" 40 | rm "${versioned_tool_path}"/bin/*-darwin "${versioned_tool_path}"/bin/*.exe "${versioned_tool_path}"/bin/*.bat 41 | } 42 | 43 | function link_tool () { 44 | local versioned_tool_path 45 | versioned_tool_path=$(find_versioned_tool_path) 46 | 47 | shell_wrapper sbt "${versioned_tool_path}/bin" 48 | } 49 | 50 | function test_tool () { 51 | local temp_dir 52 | # https://github.com/sbt/sbt/issues/1458 53 | temp_dir="$(mktemp -d)" 54 | pushd "$temp_dir" || exit 1 55 | sbt --version 56 | popd || exit 1 57 | 58 | # fix, cleanup sbt temp data 59 | rm -rf /tmp/.sbt ~/.sbt/* "$temp_dir" 60 | } 61 | -------------------------------------------------------------------------------- /test/php/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | #-------------------------------------- 23 | # Image: base 24 | #-------------------------------------- 25 | FROM ${BASE_IMAGE} AS base 26 | 27 | RUN uname -p | tee | grep aarch64 28 | RUN touch /.dummy 29 | 30 | ARG APT_HTTP_PROXY 31 | ARG CONTAINERBASE_CDN 32 | ARG CONTAINERBASE_DEBUG 33 | ARG CONTAINERBASE_LOG_LEVEL 34 | 35 | #-------------------------------------- 36 | # Image: php 37 | #-------------------------------------- 38 | FROM base AS test-php 39 | 40 | # renovate: datasource=github-releases packageName=containerbase/php-prebuild 41 | RUN install-tool php 8.5.1 42 | #-------------------------------------- 43 | # Image: composer 44 | #-------------------------------------- 45 | FROM test-php AS test-composer 46 | 47 | # renovate: datasource=github-releases packageName=containerbase/composer-prebuild 48 | RUN install-tool composer 2.9.2 49 | 50 | #-------------------------------------- 51 | # Image: final 52 | #-------------------------------------- 53 | FROM base 54 | 55 | COPY --from=test-php /.dummy /.dummy 56 | COPY --from=test-composer /.dummy /.dummy 57 | -------------------------------------------------------------------------------- /src/cli/command/cleanup-path.ts: -------------------------------------------------------------------------------- 1 | import { Command, Option } from 'clipanion'; 2 | import { deleteAsync } from 'del'; 3 | import prettyMilliseconds from 'pretty-ms'; 4 | import { logger } from '../utils'; 5 | import { command } from './utils'; 6 | 7 | @command('containerbase-cli') 8 | export class CleanupPathCommand extends Command { 9 | static override paths = [['cleanup', 'path']]; 10 | 11 | static override usage = Command.Usage({ 12 | description: 'Cleanup passed paths.', 13 | examples: [ 14 | [ 15 | 'Cleanup multiple paths', 16 | '$0 cleanup path "/tmp/**:/var/tmp" "/some/paths/**"', 17 | ], 18 | ], 19 | }); 20 | 21 | cleanupPaths = Option.Rest({ required: 1 }); 22 | 23 | async execute(): Promise { 24 | const start = Date.now(); 25 | let error = false; 26 | const paths = this.cleanupPaths.flatMap((p) => p.split(':')); 27 | logger.info({ paths }, `Cleanup paths ...`); 28 | try { 29 | const deleted = await deleteAsync(paths, { dot: true }); 30 | logger.debug({ deleted }, 'Deleted paths'); 31 | return 0; 32 | } catch (err) { 33 | error = true; 34 | logger.debug(err); 35 | if (err instanceof Error) { 36 | logger.error(err.message); 37 | } 38 | return 1; 39 | /* v8 ignore next -- coverage bug */ 40 | } finally { 41 | if (error) { 42 | logger.fatal( 43 | `Cleanup failed in ${prettyMilliseconds(Date.now() - start)}.`, 44 | ); 45 | } else { 46 | logger.info( 47 | `Cleanup succeded in ${prettyMilliseconds(Date.now() - start)}.`, 48 | ); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/cli/command/uninstall-tool.spec.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'node:process'; 2 | import { beforeEach, describe, expect, test, vi } from 'vitest'; 3 | import { testCli } from '../../../test/di'; 4 | import { logger } from '../utils'; 5 | import { MissingVersion } from '../utils/codes'; 6 | 7 | const mocks = vi.hoisted(() => ({ 8 | uninstallTool: vi.fn(), 9 | })); 10 | 11 | vi.mock('../install-tool', () => mocks); 12 | 13 | describe('cli/command/uninstall-tool', () => { 14 | beforeEach(() => { 15 | env.IGNORED_TOOLS = 'pnpm,php'; 16 | }); 17 | 18 | test.each([ 19 | { 20 | mode: 'uninstall-tool' as const, 21 | args: [], 22 | }, 23 | { 24 | mode: 'containerbase-cli' as const, 25 | args: ['uninstall', 'tool'], 26 | }, 27 | ])('$mode $args', async ({ mode, args }) => { 28 | const cli = testCli(mode); 29 | expect(await cli.run([...(args ?? []), 'node'])).toBe(MissingVersion); 30 | 31 | expect(await cli.run([...(args ?? []), 'node', '16.13.0'])).toBe(0); 32 | expect(mocks.uninstallTool).toHaveBeenCalledTimes(1); 33 | expect(mocks.uninstallTool).toHaveBeenCalledWith({ 34 | dryRun: false, 35 | recursive: false, 36 | tool: 'node', 37 | type: undefined, 38 | version: '16.13.0', 39 | }); 40 | expect(await cli.run([...(args ?? []), 'node', '16.13.0', '-d'])).toBe(0); 41 | 42 | mocks.uninstallTool.mockRejectedValueOnce(new Error('test')); 43 | expect(await cli.run([...(args ?? []), 'node', '16.13.0'])).toBe(1); 44 | 45 | expect(await cli.run([...(args ?? []), 'php'])).toBe(0); 46 | expect(logger.info).toHaveBeenCalledWith({ tool: 'php' }, 'tool ignored'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /.github/workflows/cancel-stale-merge-queue-workflows.yml: -------------------------------------------------------------------------------- 1 | name: Cancel stale merge queue workflows 2 | on: 3 | merge_group: 4 | types: 5 | - destroyed 6 | 7 | permissions: 8 | actions: write 9 | contents: read 10 | 11 | jobs: 12 | cancel-workflows: 13 | name: Cancel Workflow Runs 14 | 15 | runs-on: ubuntu-latest 16 | 17 | if: github.event.reason != 'merged' 18 | 19 | steps: 20 | - name: Get Merge Queue Commit SHA 21 | id: get-sha 22 | run: | 23 | echo "Repository: ${{ github.repository }}" 24 | echo "Head SHA: ${{ github.sha }}" 25 | echo "Merge Group Reason: ${{ github.event.reason }}" 26 | 27 | - name: Cancel Workflow Runs by SHA 28 | run: | 29 | # Get all workflow runs for the specific SHA 30 | workflow_runs=$(curl -s -H "Authorization: Bearer ${{ github.token }}" \ 31 | "https://api.github.com/repos/${{ github.repository }}/actions/runs?head_sha=${{ github.sha }}") 32 | 33 | # Extract run IDs and cancel them (except current workflow) 34 | current_run_id="${{ github.run_id }}" 35 | echo "Current Run ID: $current_run_id" 36 | echo "$workflow_runs" | jq -r '.workflow_runs[] | select(.status != "completed") | .id' | while read -r run_id; do 37 | echo "Run ID: $run_id" 38 | if [ -n "$run_id" ] && [ "$run_id" != "$current_run_id" ]; then 39 | echo "Cancelling workflow run $run_id" 40 | curl -X POST -H "Authorization: Bearer ${{ github.token }}" \ 41 | "https://api.github.com/repos/${{ github.repository }}/actions/runs/$run_id/cancel" 42 | fi 43 | done 44 | -------------------------------------------------------------------------------- /test/erlang/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | #-------------------------------------- 23 | # Image: base 24 | #-------------------------------------- 25 | FROM ${BASE_IMAGE} AS base 26 | 27 | RUN uname -p | tee | grep aarch64 28 | RUN touch /.dummy 29 | 30 | ARG APT_HTTP_PROXY 31 | ARG CONTAINERBASE_CDN 32 | ARG CONTAINERBASE_DEBUG 33 | ARG CONTAINERBASE_LOG_LEVEL 34 | 35 | #-------------------------------------- 36 | # Image: erlang 37 | #-------------------------------------- 38 | FROM base AS test-erlang 39 | 40 | # renovate: datasource=github-releases packageName=containerbase/erlang-prebuild versioning=docker 41 | RUN install-tool erlang 27.3.4.6 42 | 43 | #-------------------------------------- 44 | # Image: elixir 45 | #-------------------------------------- 46 | FROM test-erlang AS test-elixir 47 | 48 | # renovate: datasource=github-releases packageName=elixir-lang/elixir 49 | RUN install-tool elixir 1.19.4 50 | 51 | #-------------------------------------- 52 | # Image: final 53 | #-------------------------------------- 54 | FROM base 55 | 56 | COPY --from=test-erlang /.dummy /.dummy 57 | COPY --from=test-elixir /.dummy /.dummy 58 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/utils/constants.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # defines the location of the env file that gets sourced for every command 4 | export ENV_FILE=/usr/local/etc/env 5 | # defines the location of the global bashrc 6 | export BASH_RC=/etc/bash.bashrc 7 | # defines the root directory where tools will be installed 8 | export ROOT_DIR=/usr/local 9 | # defines the directory where shims to tools will be installed 10 | export BIN_DIR=/usr/local/bin 11 | export LIB_DIR=/usr/local/lib 12 | # defines the directory where user tools will be installed 13 | # shellcheck disable=SC2153 14 | export USER_HOME="/home/${USER_NAME}" 15 | # defines the umask for folders created by the root 16 | export ROOT_UMASK=755 17 | # defines the umask fo folders created by the user 18 | export USER_UMASK=775 19 | # defines the cache folder for downloaded tools, if empty no cache will be used 20 | export CONTAINERBASE_CACHE_DIR=${CONTAINERBASE_CACHE_DIR} 21 | # defines the max amount of filled space (in percent from 0-100) that is allowed 22 | # before the installation tries to free space by cleaning the cache folder 23 | # If empty, then cache cleanup is disabled 24 | export CONTAINERBASE_MAX_ALLOCATED_DISK=${CONTAINERBASE_MAX_ALLOCATED_DISK} 25 | # defines the temp directory that will be used when the cache is not active 26 | # it is used for all downloads and will be cleaned up after each install 27 | export TEMP_DIR=/tmp 28 | 29 | # used to source helper from tools 30 | export CONTAINERBASE_DIR=/usr/local/containerbase 31 | 32 | export CONTAINERBASE_VAR_DIR=/var/lib/containerbase 33 | export CONTAINERBASE_TMP_DIR=/tmp/containerbase 34 | 35 | # Used to find matching tool downloads 36 | ARCHITECTURE=$(uname -p) 37 | export ARCHITECTURE 38 | -------------------------------------------------------------------------------- /src/cli/services/index.ts: -------------------------------------------------------------------------------- 1 | import { type Bind, Container, ContainerModule } from 'inversify'; 2 | import { AptService } from './apt.service'; 3 | import { CompressionService } from './compression.service'; 4 | import { DataService } from './data.service'; 5 | import { EnvService } from './env.service'; 6 | import { HttpService } from './http.service'; 7 | import { IpcClient, IpcServer } from './ipc.service'; 8 | import { LinkToolService, type ShellWrapperConfig } from './link-tool.service'; 9 | import { PathService } from './path.service'; 10 | import { V2ToolService } from './v2-tool.service'; 11 | import { VersionService } from './version.service'; 12 | 13 | export { 14 | AptService, 15 | CompressionService, 16 | EnvService, 17 | HttpService, 18 | PathService, 19 | V2ToolService, 20 | VersionService, 21 | LinkToolService, 22 | type ShellWrapperConfig, 23 | IpcClient, 24 | IpcServer, 25 | }; 26 | 27 | function init(options: T): void { 28 | options.bind(AptService).toSelf(); 29 | options.bind(CompressionService).toSelf(); 30 | options.bind(DataService).toSelf(); 31 | options.bind(EnvService).toSelf(); 32 | options.bind(HttpService).toSelf(); 33 | options.bind(PathService).toSelf(); 34 | options.bind(V2ToolService).toSelf(); 35 | options.bind(VersionService).toSelf(); 36 | options.bind(LinkToolService).toSelf(); 37 | options.bind(IpcServer).toSelf(); 38 | options.bind(IpcClient).toSelf(); 39 | } 40 | 41 | export const rootContainerModule = new ContainerModule(init); 42 | 43 | const rootContainer = new Container(); 44 | init(rootContainer); 45 | 46 | export function createContainer(parent = rootContainer): Container { 47 | return new Container({ parent }); 48 | } 49 | -------------------------------------------------------------------------------- /src/cli/tools/ruby/cocoapods.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path'; 2 | import { injectFromHierarchy, injectable } from 'inversify'; 3 | import { semverSatisfies } from '../../utils'; 4 | import { RubyBaseInstallService, RubyGemVersionResolver } from './utils'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class CocoapodsInstallService extends RubyBaseInstallService { 9 | override readonly name: string = 'cocoapods'; 10 | 11 | override async test(_version: string): Promise { 12 | await this._spawn('pod', ['--version', '--allow-root']); 13 | } 14 | 15 | protected override async _postInstall( 16 | gem: string, 17 | version: string, 18 | prefix: string, 19 | env: NodeJS.ProcessEnv, 20 | ): Promise { 21 | // https://github.com/containerbase/base/issues/1547 22 | if (!semverSatisfies(version, '1.12.0 - 1.13.0')) { 23 | return; 24 | } 25 | 26 | await this._spawn( 27 | gem, 28 | [ 29 | 'install', 30 | 'activesupport', 31 | '--install-dir', 32 | prefix, 33 | '--bindir', 34 | join(prefix, 'bin'), 35 | '--version', 36 | '<7.1.0', 37 | ], 38 | { env }, 39 | ); 40 | 41 | await this._spawn( 42 | gem, 43 | [ 44 | 'uninstall', 45 | 'activesupport', 46 | '--install-dir', 47 | prefix, 48 | '--bindir', 49 | join(prefix, 'bin'), 50 | '--version', 51 | '>=7.1.0', 52 | ], 53 | { env }, 54 | ); 55 | } 56 | } 57 | 58 | @injectable() 59 | @injectFromHierarchy() 60 | export class CocoapodsVersionResolver extends RubyGemVersionResolver { 61 | override readonly tool: string = 'cocoapods'; 62 | } 63 | -------------------------------------------------------------------------------- /src/cli/command/init-tool.ts: -------------------------------------------------------------------------------- 1 | import { Command, Option } from 'clipanion'; 2 | import prettyMilliseconds from 'pretty-ms'; 3 | import { initializeTools } from '../prepare-tool'; 4 | import { logger } from '../utils'; 5 | import { command } from './utils'; 6 | 7 | @command('containerbase-cli') 8 | export class InitToolCommand extends Command { 9 | static override paths = [['init', 'tool']]; 10 | 11 | static override usage = Command.Usage({ 12 | description: 13 | 'Initialize a tool into the container. This creates missing files and directories.', 14 | examples: [ 15 | ['Initialize node', '$0 init tool node'], 16 | ['Initialize all prepared tools', '$0 init tool all'], 17 | ], 18 | }); 19 | 20 | tools = Option.Rest({ required: 1 }); 21 | 22 | dryRun = Option.Boolean('-d,--dry-run', false); 23 | 24 | async execute(): Promise { 25 | const start = Date.now(); 26 | let error = false; 27 | logger.info(`Initializing tools ${this.tools.join(', ')}...`); 28 | try { 29 | return await initializeTools(this.tools, this.dryRun); 30 | } catch (err) { 31 | error = true; 32 | logger.debug(err); 33 | if (err instanceof Error) { 34 | logger.fatal(err.message); 35 | } 36 | return 1; 37 | /* v8 ignore next -- coverage bug */ 38 | } finally { 39 | if (error) { 40 | logger.fatal( 41 | `Initialize tools ${this.tools.join(', ')} failed in ${prettyMilliseconds(Date.now() - start)}.`, 42 | ); 43 | } else { 44 | logger.info( 45 | `Initialize tools ${this.tools.join(', ')} succeded in ${prettyMilliseconds(Date.now() - start)}.`, 46 | ); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/cli/tools/devbox.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | import { injectFromHierarchy, injectable } from 'inversify'; 4 | import { BaseInstallService } from '../install-tool/base-install.service'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class DevboxInstallService extends BaseInstallService { 9 | readonly name = 'devbox'; 10 | 11 | override async install(version: string): Promise { 12 | const baseUrl = `https://github.com/jetify-com/devbox/releases/download/${version}/`; 13 | const filename = `devbox_${version}_linux_${this.envSvc.arch}.tar.gz`; 14 | 15 | const checksumFile = await this.http.download({ 16 | url: `${baseUrl}checksums.txt`, 17 | }); 18 | const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')) 19 | .split('\n') 20 | .find((l) => l.includes(filename)) 21 | ?.split(' ')[0]; 22 | 23 | const file = await this.http.download({ 24 | url: `${baseUrl}${filename}`, 25 | checksumType: 'sha256', 26 | expectedChecksum, 27 | }); 28 | 29 | await this.pathSvc.ensureToolPath(this.name); 30 | 31 | const path = join( 32 | await this.pathSvc.createVersionedToolPath(this.name, version), 33 | 'bin', 34 | ); 35 | await fs.mkdir(path); 36 | await this.compress.extract({ 37 | file, 38 | cwd: path, 39 | }); 40 | } 41 | 42 | override async link(version: string): Promise { 43 | const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin'); 44 | await this.shellwrapper({ srcDir: src }); 45 | } 46 | 47 | override async test(_version: string): Promise { 48 | await this._spawn(this.name, ['version']); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/utils/ruby.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function check_tool_requirements () { 4 | check_command ruby 5 | check_semver "$TOOL_VERSION" "all" 6 | } 7 | 8 | function find_gem_versioned_path() { 9 | local ruby_version 10 | local tool_dir 11 | ruby_version=$(get_tool_version ruby) 12 | tool_dir="$(find_versioned_tool_path)/${ruby_version}" 13 | 14 | if [[ -d "${tool_dir}" ]]; then 15 | echo "${tool_dir}" 16 | fi 17 | } 18 | 19 | function check_tool_installed() { 20 | test -n "$(find_gem_versioned_path)" 21 | } 22 | 23 | function install_tool() { 24 | # always install with user umask 25 | # shellcheck disable=SC2034 26 | local ROOT_UMASK=${USER_UMASK} 27 | local ruby_version 28 | local tool_path 29 | ruby_version=$(get_tool_version ruby) 30 | tool_path="$(create_versioned_tool_path)/${ruby_version}" 31 | mkdir -p "${tool_path}" 32 | 33 | if [[ $(restore_folder_from_cache "${tool_path}" "${TOOL_NAME}/${TOOL_VERSION}/${ruby_version}") -ne 0 ]]; then 34 | # restore from cache not possible 35 | # either not in cache or error, install 36 | 37 | gem install --install-dir "${tool_path}" --bindir "${tool_path}/bin" "${TOOL_NAME}" -v "${TOOL_VERSION}" # --silent 38 | 39 | # TODO: clear gem cache 40 | 41 | # store in cache 42 | cache_folder "${tool_path}" "${TOOL_NAME}/${TOOL_VERSION}/${ruby_version}" 43 | fi 44 | } 45 | 46 | function post_install () { 47 | local tool_path 48 | 49 | tool_path=$(find_gem_versioned_path) 50 | 51 | while IFS= read -r -d '' tool 52 | do 53 | [ -e "${tool_path}/bin/$tool" ] || continue 54 | shell_wrapper "$tool" "${tool_path}/bin" "GEM_PATH=\$GEM_PATH:${tool_path}" 55 | done < <(find "${tool_path}/bin" -type f -printf "%f\0") 56 | } 57 | -------------------------------------------------------------------------------- /test/http-mock.ts: -------------------------------------------------------------------------------- 1 | import type { Url } from 'node:url'; 2 | import nock from 'nock'; // eslint-disable-line no-restricted-imports 3 | import { afterAll, afterEach, beforeAll } from 'vitest'; 4 | 5 | type BasePath = string | RegExp | Url; 6 | let missingLog: string[] = []; 7 | 8 | interface TestRequest { 9 | method: string; 10 | href: string; 11 | } 12 | 13 | function onMissing(req: TestRequest, opts?: TestRequest): void { 14 | if (opts) { 15 | missingLog.push(` ${opts.method} ${opts.href}`); 16 | } else { 17 | missingLog.push(` ${req.method} ${req.href}`); 18 | } 19 | } 20 | 21 | /** 22 | * Clear nock state. Will be called in `afterEach` 23 | * @argument throwOnPending Use `false` to simply clear mocks. 24 | */ 25 | export function clear(throwOnPending = true): void { 26 | const isDone = nock.isDone(); 27 | const pending = nock.pendingMocks(); 28 | nock.abortPendingRequests(); 29 | nock.cleanAll(); 30 | const missing = missingLog; 31 | missingLog = []; 32 | if (missing.length && throwOnPending) { 33 | throw new Error(`Missing mocks!\n * ${missing.join('\n * ')}`); 34 | } 35 | if (!isDone && throwOnPending) { 36 | throw new Error(`Pending mocks!\n * ${pending.join('\n * ')}`); 37 | } 38 | } 39 | 40 | export function scope(basePath: BasePath, options?: nock.Options): nock.Scope { 41 | return nock(basePath, options); 42 | } 43 | 44 | // init nock 45 | beforeAll(() => { 46 | nock.emitter.on('no match', onMissing); 47 | nock.disableNetConnect(); 48 | }); 49 | 50 | // clean nock to clear memory leack from http module patching 51 | afterAll(() => { 52 | nock.emitter.removeListener('no match', onMissing); 53 | nock.restore(); 54 | }); 55 | 56 | // clear nock state 57 | afterEach(() => { 58 | clear(); 59 | }); 60 | -------------------------------------------------------------------------------- /src/cli/tools/skopeo.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | import { injectFromHierarchy, injectable } from 'inversify'; 4 | import { BaseInstallService } from '../install-tool/base-install.service'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class SkopeoInstallService extends BaseInstallService { 9 | readonly name = 'skopeo'; 10 | 11 | private get ghArch(): string { 12 | switch (this.envSvc.arch) { 13 | case 'arm64': 14 | return 'aarch64'; 15 | case 'amd64': 16 | return 'x86_64'; 17 | } 18 | } 19 | 20 | override async install(version: string): Promise { 21 | const name = this.name; 22 | const filename = `${name}-${version}-${this.ghArch}.tar.xz`; 23 | const url = `https://github.com/containerbase/${name}-prebuild/releases/download/${version}/${filename}`; 24 | const checksumFileUrl = `${url}.sha512`; 25 | 26 | const checksumFile = await this.http.download({ url: checksumFileUrl }); 27 | const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')).trim(); 28 | const file = await this.http.download({ 29 | url, 30 | checksumType: 'sha512', 31 | expectedChecksum, 32 | }); 33 | await this.compress.extract({ file, cwd: await this.getToolPath() }); 34 | } 35 | 36 | override async link(version: string): Promise { 37 | const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin'); 38 | await this.shellwrapper({ srcDir: src }); 39 | } 40 | 41 | override async test(_version: string): Promise { 42 | await this._spawn('skopeo', ['--version']); 43 | } 44 | 45 | private async getToolPath(): Promise { 46 | return await this.pathSvc.ensureToolPath(this.name); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/cli/tools/index.ts: -------------------------------------------------------------------------------- 1 | import type { InstallToolType } from '../utils'; 2 | 3 | export const NoPrepareTools = [ 4 | 'apko', 5 | 'bazelisk', 6 | 'bower', 7 | 'buildx', 8 | 'bun', 9 | 'bundler', 10 | 'checkov', 11 | 'cocoapods', 12 | 'composer', 13 | 'copier', 14 | 'corepack', 15 | 'deno', 16 | 'devbox', 17 | 'docker-compose', 18 | 'flux', 19 | 'git-lfs', 20 | 'gleam', 21 | 'gradle', 22 | 'hashin', 23 | 'helm', 24 | 'helmfile', 25 | 'jb', 26 | 'kubectl', 27 | 'kustomize', 28 | 'lerna', 29 | 'maven', 30 | 'nix', 31 | 'npm', 32 | 'pdm', 33 | 'pip-tools', 34 | 'pipenv', 35 | 'pnpm', 36 | 'pixi', 37 | 'poetry', 38 | 'protoc', 39 | 'renovate', 40 | 'scala', 41 | 'skopeo', 42 | 'sops', 43 | 'terraform', 44 | 'tofu', 45 | 'uv', 46 | 'vendir', 47 | 'wally', 48 | 'yarn', 49 | 'yarn-slim', 50 | ]; 51 | 52 | export const NoInitTools = [ 53 | ...NoPrepareTools, 54 | 'erlang', 55 | 'powershell', 56 | 'python', 57 | ]; 58 | 59 | /** 60 | * Tools in this map are implicit mapped from `install-tool` to `install-`. 61 | * So no need for an extra install service. 62 | */ 63 | export const ResolverMap: Record = { 64 | bundler: 'gem', 65 | checkov: 'pip', 66 | copier: 'pip', 67 | corepack: 'npm', 68 | hashin: 'pip', 69 | npm: 'npm', 70 | pnpm: 'npm', 71 | pdm: 'pip', 72 | 'pip-tools': 'pip', 73 | pipenv: 'pip', 74 | poetry: 'pip', 75 | uv: 'pip', 76 | }; 77 | 78 | /** 79 | * This tools are deprecated and should not be used anymore via `install-tool`. 80 | * They are implicit mapped from `install-tool` to `install-`. 81 | */ 82 | export const DeprecatedTools: Record = { 83 | bower: 'npm', 84 | lerna: 'npm', 85 | }; 86 | -------------------------------------------------------------------------------- /src/cli/tools/sops.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | import { injectFromHierarchy, injectable } from 'inversify'; 4 | import { BaseInstallService } from '../install-tool/base-install.service'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class SopsInstallService extends BaseInstallService { 9 | readonly name = 'sops'; 10 | 11 | override async install(version: string): Promise { 12 | const baseUrl = `https://github.com/getsops/${this.name}/releases/download/v${version}/`; 13 | const filename = `${this.name}-v${version}.linux.${this.envSvc.arch}`; 14 | 15 | const checksumFile = await this.http.download({ 16 | url: `${baseUrl}${this.name}-v${version}.checksums.txt`, 17 | }); 18 | const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')) 19 | .split('\n') 20 | .find((l) => l.includes(filename)) 21 | ?.split(' ')[0]; 22 | 23 | const file = await this.http.download({ 24 | url: `${baseUrl}${filename}`, 25 | checksumType: 'sha256', 26 | expectedChecksum, 27 | }); 28 | 29 | await this.pathSvc.ensureToolPath(this.name); 30 | 31 | const path = join( 32 | await this.pathSvc.createVersionedToolPath(this.name, version), 33 | 'bin', 34 | ); 35 | await fs.mkdir(path); 36 | await fs.copyFile(file, join(path, this.name)); 37 | await fs.chmod(join(path, this.name), this.envSvc.umask); 38 | } 39 | 40 | override async link(version: string): Promise { 41 | const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin'); 42 | 43 | await this.shellwrapper({ srcDir: src }); 44 | } 45 | 46 | override async test(_version: string): Promise { 47 | await this._spawn(this.name, ['--version']); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/cli/tools/flux.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | import { injectFromHierarchy, injectable } from 'inversify'; 4 | import { BaseInstallService } from '../install-tool/base-install.service'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class FluxInstallService extends BaseInstallService { 9 | readonly name = 'flux'; 10 | 11 | private get arch(): string { 12 | return this.envSvc.arch; 13 | } 14 | 15 | override async install(version: string): Promise { 16 | const baseUrl = `https://github.com/fluxcd/flux2/releases/download/v${version}/`; 17 | const filename = `flux_${version}_linux_${this.arch}.tar.gz`; 18 | 19 | const checksumFile = await this.http.download({ 20 | url: `${baseUrl}flux_${version}_checksums.txt`, 21 | }); 22 | const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')) 23 | .split('\n') 24 | .find((l) => l.includes(filename)) 25 | ?.split(' ')[0]; 26 | 27 | const file = await this.http.download({ 28 | url: `${baseUrl}${filename}`, 29 | checksumType: 'sha256', 30 | expectedChecksum, 31 | }); 32 | 33 | await this.pathSvc.ensureToolPath(this.name); 34 | 35 | const path = join( 36 | await this.pathSvc.createVersionedToolPath(this.name, version), 37 | 'bin', 38 | ); 39 | await fs.mkdir(path); 40 | await this.compress.extract({ 41 | file, 42 | cwd: path, 43 | }); 44 | } 45 | 46 | override async link(version: string): Promise { 47 | const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin'); 48 | 49 | await this.shellwrapper({ srcDir: src }); 50 | } 51 | 52 | override async test(_version: string): Promise { 53 | await this._spawn('flux', ['--version']); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/bash/linking.bats: -------------------------------------------------------------------------------- 1 | # shellcheck disable=SC2148 2 | 3 | setup() { 4 | load "$BATS_SUPPORT_LOAD_PATH" 5 | load "$BATS_ASSERT_LOAD_PATH" 6 | 7 | TEST_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")" >/dev/null 2>&1 && pwd)" 8 | TEST_ROOT_DIR=$(mktemp -u) 9 | 10 | load "$TEST_DIR/../../src/usr/local/containerbase/util.sh" 11 | 12 | # load test overwrites 13 | load "$TEST_DIR/util.sh" 14 | 15 | mkdir "${ROOT_DIR}/bin" 16 | 17 | setup_directories 18 | } 19 | 20 | teardown() { 21 | rm -rf "${TEST_ROOT_DIR}" 22 | } 23 | 24 | @test "link_wrapper" { 25 | 26 | mkdir -p "${USER_HOME}/bin" 27 | mkdir -p "${USER_HOME}/bin2" 28 | mkdir -p "${USER_HOME}/bin3" 29 | 30 | run link_wrapper 31 | assert_failure 32 | 33 | run link_wrapper foo 34 | assert_failure 35 | 36 | run link_wrapper git 37 | assert_success 38 | assert [ -f "${BIN_DIR}/git" ] 39 | 40 | printf "#!/bin/bash\n\necho 'foobar'" > "${USER_HOME}/bin2/foobar" 41 | chmod +x "${USER_HOME}/bin2/foobar" 42 | 43 | run link_wrapper foobar "${USER_HOME}/bin2/foobar" 44 | assert_success 45 | assert [ -f "${BIN_DIR}/foobar" ] 46 | rm "${BIN_DIR}/foobar" 47 | 48 | printf "#!/bin/bash\n\necho 'foobar'" > "${USER_HOME}/bin3/foobar" 49 | chmod +x "${USER_HOME}/bin3/foobar" 50 | 51 | run link_wrapper foobar "${USER_HOME}/bin3" 52 | assert_success 53 | assert [ -f "${BIN_DIR}/foobar" ] 54 | 55 | } 56 | 57 | @test "shell_wrapper" { 58 | 59 | mkdir -p "${USER_HOME}/bin" 60 | printf "#!/bin/bash\n\necho 'foobar'" > "${USER_HOME}/bin/foobar" 61 | chmod +x "${USER_HOME}/bin/foobar" 62 | 63 | run shell_wrapper 64 | assert_failure 65 | assert_output --partial "param SOURCE is set but empty" 66 | 67 | run shell_wrapper foo 68 | assert_failure 69 | assert_output --partial "param SOURCE is set but empty" 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/cli/services/apt.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'node:process'; 2 | import type { Container } from 'inversify'; 3 | import { beforeEach, describe, expect, test, vi } from 'vitest'; 4 | import { AptService } from '.'; 5 | import { testContainer } from '~test/di'; 6 | 7 | const mocks = vi.hoisted(() => ({ 8 | spawn: vi.fn(), 9 | rm: vi.fn(), 10 | writeFile: vi.fn(), 11 | })); 12 | 13 | vi.mock('nano-spawn', () => ({ default: mocks.spawn })); 14 | vi.mock('node:fs/promises', async (importActual) => ({ 15 | default: { ...(await importActual()), ...mocks }, 16 | ...mocks, 17 | })); 18 | 19 | describe('cli/services/apt.service', () => { 20 | let child!: Container; 21 | let svc!: AptService; 22 | 23 | beforeEach(async () => { 24 | child = await testContainer(); 25 | svc = await child.getAsync(AptService); 26 | delete env.APT_HTTP_PROXY; 27 | }); 28 | 29 | test('skips install', async () => { 30 | mocks.spawn.mockResolvedValueOnce({ 31 | stdout: 'Status: install ok installed', 32 | }); 33 | await svc.install('some-pkg'); 34 | expect(mocks.spawn).toHaveBeenCalledTimes(1); 35 | }); 36 | 37 | test('works', async () => { 38 | mocks.spawn.mockRejectedValueOnce(new Error('not installed')); 39 | await svc.install('some-pkg'); 40 | expect(mocks.spawn).toHaveBeenCalledTimes(3); 41 | expect(mocks.writeFile).not.toHaveBeenCalled(); 42 | expect(mocks.rm).not.toHaveBeenCalled(); 43 | }); 44 | 45 | test('uses proxy', async () => { 46 | env.APT_HTTP_PROXY = 'http://proxy'; 47 | mocks.spawn.mockRejectedValueOnce(new Error('not installed')); 48 | await svc.install('some-pkg', 'other-pkg'); 49 | expect(mocks.spawn).toHaveBeenCalledTimes(4); 50 | expect(mocks.writeFile).toHaveBeenCalledOnce(); 51 | expect(mocks.rm).toHaveBeenCalledOnce(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/cli/tools/kustomize.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | import { injectFromHierarchy, injectable } from 'inversify'; 4 | import { BaseInstallService } from '../install-tool/base-install.service'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class KustomizeInstallService extends BaseInstallService { 9 | readonly name = 'kustomize'; 10 | 11 | override async install(version: string): Promise { 12 | const name = this.name; 13 | const filename = `${name}_v${version}_linux_${this.envSvc.arch}.tar.gz`; 14 | const baseUrl = `https://github.com/kubernetes-sigs/${name}/releases/download/${name}%2Fv${version}/`; 15 | 16 | const checksumFile = await this.http.download({ 17 | url: `${baseUrl}checksums.txt`, 18 | fileName: `${name}_v${version}_checksums.txt`, 19 | }); 20 | const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')) 21 | .split('\n') 22 | .find((l) => l.includes(filename)) 23 | ?.split(' ')[0]; 24 | 25 | const file = await this.http.download({ 26 | url: `${baseUrl}${filename}`, 27 | checksumType: 'sha256', 28 | expectedChecksum, 29 | }); 30 | await this.pathSvc.ensureToolPath(this.name); 31 | const cwd = path.join( 32 | await this.pathSvc.createVersionedToolPath(this.name, version), 33 | 'bin', 34 | ); 35 | await fs.mkdir(cwd); 36 | await this.compress.extract({ file, cwd }); 37 | } 38 | 39 | override async link(version: string): Promise { 40 | const src = path.join( 41 | this.pathSvc.versionedToolPath(this.name, version), 42 | 'bin', 43 | ); 44 | await this.shellwrapper({ srcDir: src }); 45 | } 46 | 47 | override async test(_version: string): Promise { 48 | await this._spawn(this.name, ['version']); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/cli/tools/pixi.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | import { injectFromHierarchy, injectable } from 'inversify'; 4 | import { BaseInstallService } from '../install-tool/base-install.service'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class PixiInstallService extends BaseInstallService { 9 | readonly name = 'pixi'; 10 | 11 | private get ghArch(): string { 12 | switch (this.envSvc.arch) { 13 | case 'arm64': 14 | return 'aarch64'; 15 | case 'amd64': 16 | return 'x86_64'; 17 | } 18 | } 19 | 20 | override async install(version: string): Promise { 21 | const url = `https://github.com/prefix-dev/pixi/releases/download/v${version}/${this.name}-${this.ghArch}-unknown-linux-musl.tar.gz`; 22 | const checksumFileUrl = `${url}.sha256`; 23 | 24 | const checksumFile = await this.http.download({ url: checksumFileUrl }); 25 | const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')) 26 | .trim() 27 | .split(' ')[0]; 28 | 29 | const file = await this.http.download({ 30 | url, 31 | checksumType: 'sha256', 32 | expectedChecksum, 33 | }); 34 | 35 | await this.pathSvc.ensureToolPath(this.name); 36 | 37 | const path = join( 38 | await this.pathSvc.createVersionedToolPath(this.name, version), 39 | 'bin', 40 | ); 41 | await fs.mkdir(path); 42 | await this.compress.extract({ 43 | file, 44 | cwd: path, 45 | }); 46 | } 47 | 48 | override async link(version: string): Promise { 49 | const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin'); 50 | 51 | await this.shellwrapper({ srcDir: src }); 52 | } 53 | 54 | override async test(_version: string): Promise { 55 | await this._spawn(this.name, ['--version']); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/cli/command/link-tool.spec.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'node:process'; 2 | import { Cli } from 'clipanion'; 3 | import { beforeEach, describe, expect, test, vi } from 'vitest'; 4 | import { logger } from '../utils'; 5 | import { registerCommands } from '.'; 6 | 7 | const mocks = vi.hoisted(() => ({ 8 | linkTool: vi.fn(), 9 | })); 10 | 11 | vi.mock('../install-tool', () => mocks); 12 | 13 | describe('cli/command/link-tool', () => { 14 | const cli = new Cli({ binaryName: 'containerbase-cli' }); 15 | registerCommands(cli, 'containerbase-cli'); 16 | 17 | beforeEach(() => { 18 | delete env.TOOL_NAME; 19 | delete env.TOOL_VERSION; 20 | }); 21 | 22 | test('missing TOOL_NAME', async () => { 23 | expect(await cli.run(['lt', 'node', 'bin'])).toBe(1); 24 | expect(mocks.linkTool).not.toHaveBeenCalled(); 25 | expect(logger.error).toHaveBeenCalledExactlyOnceWith( 26 | `Missing 'TOOL_NAME' environment variable`, 27 | ); 28 | }); 29 | test('missing TOOL_VERSION', async () => { 30 | env.TOOL_NAME = 'node'; 31 | expect(await cli.run(['lt', 'node', 'bin'])).toBe(1); 32 | expect(mocks.linkTool).not.toHaveBeenCalled(); 33 | expect(logger.error).toHaveBeenCalledExactlyOnceWith( 34 | `Missing 'TOOL_VERSION' environment variable`, 35 | ); 36 | }); 37 | 38 | test('works', async () => { 39 | env.TOOL_NAME = 'node'; 40 | env.TOOL_VERSION = '1.2.3'; 41 | 42 | expect(await cli.run(['lt', 'node', 'bin'])).toBe(0); 43 | expect(mocks.linkTool).toHaveBeenCalledExactlyOnceWith('node', { 44 | name: 'node', 45 | srcDir: 'bin', 46 | }); 47 | }); 48 | 49 | test('fails', async () => { 50 | env.TOOL_NAME = 'node'; 51 | env.TOOL_VERSION = '1.2.3'; 52 | mocks.linkTool.mockRejectedValueOnce(new Error('test')); 53 | expect(await cli.run(['lt', 'node', 'bin'])).toBe(1); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/cli/tools/kubectl.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | import { injectFromHierarchy, injectable } from 'inversify'; 4 | import { BaseInstallService } from '../install-tool/base-install.service'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class KubectlInstallService extends BaseInstallService { 9 | readonly name = 'kubectl'; 10 | 11 | override async install(version: string): Promise { 12 | const baseUrl = `https://dl.k8s.io/release/v${version}/bin/linux/${this.envSvc.arch}/`; 13 | const filename = this.name; 14 | 15 | const checksumFile = await this.http.download({ 16 | url: `${baseUrl}${filename}.sha256`, 17 | fileName: `${filename}-v${version}-${this.envSvc.arch}.sha256`, 18 | }); 19 | const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')) 20 | .split('\n') 21 | .find((l) => l.includes(filename)) 22 | ?.split(' ')[0]; 23 | 24 | const file = await this.http.download({ 25 | url: `${baseUrl}${filename}`, 26 | fileName: `${filename}-v${version}-${this.envSvc.arch}`, 27 | checksumType: 'sha256', 28 | expectedChecksum, 29 | }); 30 | 31 | await this.pathSvc.ensureToolPath(this.name); 32 | 33 | const path = join( 34 | await this.pathSvc.createVersionedToolPath(this.name, version), 35 | 'bin', 36 | ); 37 | await fs.mkdir(path); 38 | await fs.copyFile(file, join(path, filename)); 39 | await fs.chmod(join(path, filename), this.envSvc.umask); 40 | } 41 | 42 | override async link(version: string): Promise { 43 | const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin'); 44 | 45 | await this.shellwrapper({ srcDir: src }); 46 | } 47 | 48 | override async test(_version: string): Promise { 49 | await this._spawn(this.name, ['version', '--client']); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/utils/v2/filesystem.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file will overwrite certain functionality that is required for v2 tools 4 | # e.g. for v2 tools we only support a single install directory for root and user installs 5 | # Whenever a v2 tool is installed, this file gets sourced 6 | 7 | # OVERWRITE: 8 | # 9 | # Will always return the root dir, no matter what user is calling the function 10 | function get_install_dir () { 11 | echo "${ROOT_DIR}" 12 | } 13 | 14 | # OVERWRITE: 15 | # 16 | # Will return the path to the tools path, which is {installdir}/tools/{toolname} instead of {installdir}/{toolname} 17 | function find_tool_path () { 18 | local tools_path 19 | tools_path=$(get_tools_path) 20 | 21 | if [[ -d "${tools_path}/${TOOL_NAME}" ]]; then 22 | echo "${tools_path}/${TOOL_NAME}" 23 | fi 24 | } 25 | 26 | # OVERWRITE: 27 | # 28 | # Creates the tool path in {installdir}/tools/{toolname} with 775 instead of in {installdir}/{toolname} with default umask 29 | function create_tool_path () { 30 | local tools_path 31 | tools_path=$(get_tools_path) 32 | 33 | if [ -d "${tools_path}/${TOOL_NAME}" ]; then 34 | echo "${tools_path}/${TOOL_NAME}" 35 | return 36 | fi 37 | 38 | create_folder "${tools_path}/${TOOL_NAME}" 775 39 | echo "${tools_path}/${TOOL_NAME}" 40 | } 41 | 42 | # OVERWRITE: 43 | # 44 | # Creates the versioned tool path in {installdir}/tools/{toolname}/{version} with user specific umask 45 | # instead of in {installdir}/{toolname}/{version} with default umask 46 | function create_versioned_tool_path () { 47 | local tool_path 48 | tool_path=$(create_tool_path) 49 | 50 | if [ -d "${tool_path}/${TOOL_VERSION}" ]; then 51 | echo "${tool_path}/${TOOL_VERSION}" 52 | return 53 | fi 54 | 55 | local umask 56 | umask=$(get_umask) 57 | 58 | mkdir -m "${umask}" "${tool_path}/${TOOL_VERSION}" 59 | echo "${tool_path}/${TOOL_VERSION}" 60 | } 61 | -------------------------------------------------------------------------------- /src/cli/tools/bun.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | import { injectFromHierarchy, injectable } from 'inversify'; 4 | import { BaseInstallService } from '../install-tool/base-install.service'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class BunInstallService extends BaseInstallService { 9 | readonly name = 'bun'; 10 | 11 | private get ghArch(): string { 12 | switch (this.envSvc.arch) { 13 | case 'arm64': 14 | return 'aarch64'; 15 | case 'amd64': 16 | return 'x64'; 17 | } 18 | } 19 | 20 | override async install(version: string): Promise { 21 | const baseUrl = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/`; 22 | const filename = `bun-linux-${this.ghArch}.zip`; 23 | 24 | const checksumFile = await this.http.download({ 25 | url: `${baseUrl}SHASUMS256.txt`, 26 | }); 27 | const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')) 28 | .split('\n') 29 | .find((l) => l.includes(filename)) 30 | ?.split(' ')[0]; 31 | 32 | const file = await this.http.download({ 33 | url: `${baseUrl}${filename}`, 34 | checksumType: 'sha256', 35 | expectedChecksum, 36 | }); 37 | 38 | await this.pathSvc.ensureToolPath(this.name); 39 | 40 | const path = join( 41 | await this.pathSvc.createVersionedToolPath(this.name, version), 42 | 'bin', 43 | ); 44 | await fs.mkdir(path); 45 | await this.compress.extract({ 46 | file, 47 | cwd: path, 48 | strip: 1, 49 | }); 50 | } 51 | 52 | override async link(version: string): Promise { 53 | const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin'); 54 | 55 | await this.shellwrapper({ srcDir: src }); 56 | } 57 | 58 | override async test(_version: string): Promise { 59 | await this._spawn(this.name, ['--version']); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/usr/local/containerbase/utils/v2/defaults.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file contains all functions a tool must implement to be properly supported 4 | # The containerbase scripts rely on the functions to decide if a tool needs to be handled in a special way 5 | # This defaults are loaded before any tool file so not overwriting the function will 6 | # result in the install process being aborted 7 | 8 | # Is used to check if all requirements are met to install the tool 9 | function check_tool_requirements () { 10 | # Sensitive default that can be overwritten by tools if needed 11 | check_semver "${TOOL_VERSION}" all 12 | } 13 | 14 | # Is used to check if the tool has already been installed in the given version 15 | function check_tool_installed () { 16 | # Sensitive default that can be overwritten by tools if needed 17 | test -n "$(find_versioned_tool_path)" 18 | } 19 | 20 | # Installs the tool with the given version 21 | function install_tool () { 22 | echo "'install_tool' not defined for tool ${TOOL_NAME}" 23 | exit 1 24 | } 25 | 26 | # Links the tools installation to the global bin folders 27 | function link_tool () { 28 | echo "'link_tool' not defined for tool ${TOOL_NAME}" 29 | exit 1 30 | } 31 | 32 | # Installs needed packages to make the tool runtime installable 33 | function prepare_tool() { 34 | true 35 | } 36 | 37 | # creates required files and folders for the tool 38 | function init_tool() { 39 | true 40 | } 41 | 42 | # Called after install_tool and link_tool. It's always called. 43 | # Allow tools to do some additional stuff, like overwriting additional shell wrapper 44 | function post_install () { 45 | true 46 | } 47 | 48 | # Called after install_tool and link_tool. It's not called when `SKIP_VERSION` is set. 49 | # Allow tools to do some testing 50 | function test_tool () { 51 | true 52 | } 53 | 54 | # Uninstalls additional things from the tool with the given version 55 | function uninstall_tool () { 56 | true 57 | } 58 | -------------------------------------------------------------------------------- /src/cli/services/apt.service.ts: -------------------------------------------------------------------------------- 1 | import { rm, writeFile } from 'fs/promises'; 2 | import { join } from 'node:path'; 3 | import { inject, injectable } from 'inversify'; 4 | import { logger, spawn } from '../utils'; 5 | import { EnvService } from './env.service'; 6 | 7 | @injectable() 8 | export class AptService { 9 | @inject(EnvService) 10 | private readonly envSvc!: EnvService; 11 | 12 | async install(...packages: string[]): Promise { 13 | const todo: string[] = []; 14 | 15 | for (const pkg of packages) { 16 | if (await this.isInstalled(pkg)) { 17 | continue; 18 | } 19 | todo.push(pkg); 20 | } 21 | 22 | if (todo.length === 0) { 23 | logger.debug({ packages }, 'all packages already installed'); 24 | return; 25 | } 26 | 27 | logger.debug({ packages: todo }, 'installing packages'); 28 | 29 | if (this.envSvc.aptProxy) { 30 | logger.debug({ proxy: this.envSvc.aptProxy }, 'using apt proxy'); 31 | await writeFile( 32 | join( 33 | this.envSvc.rootDir, 34 | 'etc/apt/apt.conf.d/containerbase-proxy.conf', 35 | ), 36 | `Acquire::http::Proxy "${this.envSvc.aptProxy}";\n`, 37 | ); 38 | } 39 | 40 | try { 41 | await spawn('apt-get', ['-qq', 'update']); 42 | await spawn('apt-get', ['-qq', 'install', '-y', ...todo]); 43 | } finally { 44 | if (this.envSvc.aptProxy) { 45 | await rm( 46 | join( 47 | this.envSvc.rootDir, 48 | 'etc/apt/apt.conf.d/containerbase-proxy.conf', 49 | ), 50 | { 51 | force: true, 52 | }, 53 | ); 54 | } 55 | } 56 | } 57 | 58 | private async isInstalled(pkg: string): Promise { 59 | try { 60 | const res = await spawn('dpkg', ['-s', pkg]); 61 | return res.stdout.includes('Status: install ok installed'); 62 | } catch { 63 | return false; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/cli/services/data.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { chmod, stat } from 'node:fs/promises'; 2 | import { platform } from 'node:os'; 3 | import type Nedb from '@seald-io/nedb'; 4 | import { Container } from 'inversify'; 5 | import { beforeAll, beforeEach, describe, expect, test } from 'vitest'; 6 | import { fileRights } from '../utils'; 7 | import { DataService } from './data.service'; 8 | import { testContainer } from '~test/di'; 9 | import { ensurePaths, rootPath } from '~test/path'; 10 | 11 | async function fstat(path: string): Promise { 12 | const s = await stat(path); 13 | return s.mode & fileRights; 14 | } 15 | 16 | describe('cli/services/data.service', () => { 17 | let child!: Container; 18 | let svc!: DataService; 19 | let dataDir!: string; 20 | 21 | const expectedMode = platform() === 'win32' ? 0 : 0o664; 22 | 23 | beforeAll(async () => { 24 | await ensurePaths('opt/containerbase/data'); 25 | dataDir = rootPath('opt/containerbase/data'); 26 | await chmod(dataDir, 0o775); 27 | }); 28 | 29 | beforeEach(async () => { 30 | child = await testContainer(); 31 | svc = await child.getAsync(DataService); 32 | }); 33 | 34 | test('works', async () => { 35 | expect(await fstat(dataDir)).toBe(0o775); 36 | 37 | const db = await svc.load('test'); 38 | expect(await fstat(db.filename)).toBe(expectedMode); 39 | 40 | await db.ensureIndexAsync({ fieldName: 'test' }); 41 | expect(await fstat(db.filename)).toBe(expectedMode); 42 | 43 | await (db as unknown as Nedb).compactDatafileAsync(); 44 | expect(await fstat(db.filename)).toBe(expectedMode); 45 | 46 | expect(await fstat(dataDir)).toBe(0o775); 47 | 48 | await (db as unknown as Nedb).dropDatabaseAsync(); 49 | await expect(fstat(db.filename)).rejects.toThrowError( 50 | `ENOENT: no such file or directory, stat '${rootPath('/opt/containerbase/data/test.nedb')}'`, 51 | ); 52 | expect(await fstat(dataDir)).toBe(0o775); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/powershell/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=containerbase 2 | 3 | #-------------------------------------- 4 | # Image: containerbase 5 | #-------------------------------------- 6 | FROM ghcr.io/containerbase/ubuntu:24.04 AS containerbase 7 | 8 | ENV BASH_ENV=/usr/local/etc/env 9 | SHELL ["/bin/bash" , "-c"] 10 | 11 | ARG TARGETARCH 12 | COPY dist/docker/ / 13 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli 14 | 15 | ARG APT_HTTP_PROXY 16 | ARG CONTAINERBASE_CDN 17 | ARG CONTAINERBASE_DEBUG 18 | ARG CONTAINERBASE_LOG_LEVEL 19 | 20 | RUN install-containerbase 21 | 22 | #-------------------------------------- 23 | # Image: base 24 | #-------------------------------------- 25 | FROM ${BASE_IMAGE} AS base 26 | 27 | RUN touch /.dummy 28 | 29 | ARG APT_HTTP_PROXY 30 | ARG CONTAINERBASE_CDN 31 | ARG CONTAINERBASE_DEBUG 32 | ARG CONTAINERBASE_LOG_LEVEL 33 | 34 | #-------------------------------------- 35 | # test: powershell 7.2 (non-root) 36 | #-------------------------------------- 37 | FROM base AS testa 38 | 39 | RUN prepare-tool powershell 40 | 41 | USER 12021 42 | 43 | # Don't update 44 | RUN install-tool powershell v7.2.8 45 | 46 | 47 | RUN set -ex; \ 48 | pwsh -Version 49 | 50 | RUN set -ex; \ 51 | pwsh -Command Write-Host Hello, World! 52 | 53 | SHELL [ "/bin/sh", "-c" ] 54 | RUN pwsh --version 55 | 56 | #-------------------------------------- 57 | # test: powershell 7.x 58 | #-------------------------------------- 59 | FROM base AS testb 60 | 61 | # renovate: datasource=github-releases packageName=PowerShell/PowerShell 62 | RUN install-tool powershell v7.5.4 63 | 64 | USER 12021 65 | 66 | RUN set -ex; \ 67 | pwsh -Version 68 | 69 | RUN set -ex; \ 70 | pwsh -Command Write-Host Hello, World! 71 | 72 | SHELL [ "/bin/sh", "-c" ] 73 | RUN pwsh --version 74 | 75 | 76 | #-------------------------------------- 77 | FROM base 78 | 79 | COPY --from=testa /.dummy /.dummy 80 | COPY --from=testb /.dummy /.dummy 81 | -------------------------------------------------------------------------------- /src/cli/tools/helm.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | import { injectFromHierarchy, injectable } from 'inversify'; 4 | import { BaseInstallService } from '../install-tool/base-install.service'; 5 | 6 | @injectable() 7 | @injectFromHierarchy() 8 | export class HelmInstallService extends BaseInstallService { 9 | readonly name = 'helm'; 10 | 11 | override async install(version: string): Promise { 12 | const name = this.name; 13 | const filename = `${name}-v${version}-linux-${this.envSvc.arch}.tar.gz`; 14 | const url = `https://get.helm.sh/${filename}`; 15 | 16 | const expectedChecksum = await this._getChecksum( 17 | `${url}.sha256sum`, 18 | filename, 19 | ); 20 | const file = await this.http.download({ 21 | url, 22 | checksumType: 'sha256', 23 | expectedChecksum, 24 | }); 25 | await this.pathSvc.ensureToolPath(this.name); 26 | const cwd = path.join( 27 | await this.pathSvc.createVersionedToolPath(this.name, version), 28 | 'bin', 29 | ); 30 | await fs.mkdir(cwd); 31 | await this.compress.extract({ file, cwd, strip: 1 }); 32 | } 33 | 34 | override async link(version: string): Promise { 35 | const src = path.join( 36 | this.pathSvc.versionedToolPath(this.name, version), 37 | 'bin', 38 | ); 39 | await this.shellwrapper({ srcDir: src }); 40 | } 41 | 42 | override async test(_version: string): Promise { 43 | await this._spawn(this.name, ['version']); 44 | } 45 | 46 | /** TODO: create helper */ 47 | protected async _getChecksum( 48 | url: string, 49 | filename: string, 50 | ): Promise { 51 | const checksumFile = await this.http.download({ url }); 52 | const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')) 53 | .split('\n') 54 | .find((l) => l.includes(filename)) 55 | ?.split(' ')[0]; 56 | return expectedChecksum; 57 | } 58 | } 59 | --------------------------------------------------------------------------------