├── .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 " \
15 | org.opencontainers.image.source="https://github.com/containerbase/base"
16 |
17 | # autoloading containerbase env
18 | ENV BASH_ENV=/usr/local/etc/env ENV=/usr/local/etc/env PATH=/home/ubuntu/bin:$PATH
19 | SHELL ["/bin/bash" , "-c"]
20 |
21 | ENTRYPOINT ["docker-entrypoint.sh"]
22 | CMD ["bash"]
23 |
24 | ARG TARGETARCH
25 |
26 | COPY dist/docker/ /
27 | COPY dist/cli/containerbase-cli-${TARGETARCH} /usr/local/containerbase/bin/containerbase-cli
28 |
29 | RUN install-containerbase
30 |
31 | # renovate: datasource=github-tags packageName=git/git
32 | RUN install-tool git v2.52.0
33 |
34 |
35 | LABEL org.opencontainers.image.version="${CONTAINERBASE_VERSION}"
36 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Launch Program",
11 | "skipFiles": ["/**"],
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 |
--------------------------------------------------------------------------------