diff --git a/docs/custom-registries.md b/docs/custom-registries.md index e99870c60d..ea03a3d890 100644 --- a/docs/custom-registries.md +++ b/docs/custom-registries.md @@ -712,10 +712,14 @@ Ruby releases are downloaded from: Samples: ```txt -https://github.com/containerbase/ruby-prebuild/releases/download/3.0.3/ruby-3.0.3-jammy-x86_x64.tar.xz.sha512 -https://github.com/containerbase/ruby-prebuild/releases/download/3.0.3/ruby-3.0.3-jammy-x86_x64.tar.xz +https://github.com/containerbase/ruby-prebuild/releases/download/3.4.11/ruby-3.4.11-jammy-x86_64.tar.xz.sha512 +https://github.com/containerbase/ruby-prebuild/releases/download/3.4.11/ruby-3.4.11-jammy-x86_64.tar.xz +https://github.com/containerbase/ruby-prebuild/releases/download/3.4.11/ruby-3.4.11-jammy-aarch64.tar.xz.sha512 +https://github.com/containerbase/ruby-prebuild/releases/download/3.4.11/ruby-3.4.11-jammy-aarch64.tar.xz ``` +The download is always verified against the `.sha512` checksum, so a mirror needs to provide both files. + ### `gem` tools Gem tools are downloaded from: diff --git a/docs/new-tool.md b/docs/new-tool.md index 02b88cd0d6..862b3fe422 100644 --- a/docs/new-tool.md +++ b/docs/new-tool.md @@ -250,7 +250,7 @@ Expect more discussion on the Issue for tools in this category. #### Legacy shell installers > [!NOTE] -> Do not add a `.sh` file into [`src/usr/local/containerbase/tools/v2`](../src/usr/local/containerbase/tools/v2/). +> Do not add a `.sh` file into `src/usr/local/containerbase/tools/v2`. > > These are the legacy installer formats that we are in the process of migrating away from. > When converting an existing shell tool to a TypeScript install service, see the [tool installer best practices](./tool-installer-best-practices.md) for what to keep and what not to change. diff --git a/docs/tool-installer-best-practices.md b/docs/tool-installer-best-practices.md index 17d438b125..1edd3090c3 100644 --- a/docs/tool-installer-best-practices.md +++ b/docs/tool-installer-best-practices.md @@ -8,7 +8,7 @@ See [`new-tool.md`](./new-tool.md) for the full walkthrough of adding a tool; th Every tool is a TypeScript install service extending `BaseInstallService` ([`src/cli/install-tool/base-install.service.ts`](../src/cli/install-tool/base-install.service.ts)), living under [`src/cli/tools/`](../src/cli/tools/). -Do not add new `.sh` files under [`src/usr/local/containerbase/tools/v2`](../src/usr/local/containerbase/tools/v2/). +Do not add new `.sh` files under `src/usr/local/containerbase/tools/v2`. Both the v1 and v2 shell tool formats are deprecated and being migrated to TypeScript install services. When converting a legacy shell tool, keep installing every version the shell script could install. diff --git a/src/cli/install-tool/index.ts b/src/cli/install-tool/index.ts index d1e5f18650..78213d1d51 100644 --- a/src/cli/install-tool/index.ts +++ b/src/cli/install-tool/index.ts @@ -114,7 +114,6 @@ import { TofuInstallService } from '../tools/tofu.ts'; import { VendirInstallService } from '../tools/vendir.ts'; import { WallyInstallService } from '../tools/wally.ts'; import { type InstallToolType, logger } from '../utils/index.ts'; -import { isNotKnownV2Tool } from '../utils/v2-tool.ts'; import { V1ToolInstallService, V2ToolInstallService, @@ -205,7 +204,7 @@ async function prepareInstallContainer(): Promise { // v2 tool services const pathSvc = await container.getAsync(PathService); const legacyTools = await pathSvc.findLegacyTools(); - for (const tool of legacyTools.filter(isNotKnownV2Tool)) { + for (const tool of legacyTools) { @injectable() @injectFromHierarchy() class GenericInstallService extends V2ToolInstallService { diff --git a/src/cli/prepare-tool/index.spec.ts b/src/cli/prepare-tool/index.spec.ts index 681e6fa473..d46e93cc5e 100644 --- a/src/cli/prepare-tool/index.spec.ts +++ b/src/cli/prepare-tool/index.spec.ts @@ -1,7 +1,9 @@ import fs from 'node:fs/promises'; import { beforeAll, describe, expect, test, vi } from 'vitest'; import { PathService, createContainer } from '../services/index.ts'; +import { RubyPrepareService } from '../tools/ruby/index.ts'; import { initializeTools, prepareTools } from './index.ts'; +import { V2ToolPrepareService } from './prepare-legacy-tools.service.ts'; import { ensurePaths, rootPath } from '~test/path.ts'; vi.mock('del'); @@ -37,6 +39,24 @@ describe('cli/prepare-tool/index', () => { expect(await prepareTools(['not-exist'])).toBe(1); }); + test('prefers a modern service over a v2 shell tool with the same name', async () => { + const script = rootPath('usr/local/containerbase/tools/v2/ruby.sh'); + await fs.writeFile(script, ''); + const ruby = vi + .spyOn(RubyPrepareService.prototype, 'prepare') + .mockResolvedValue(); + const v2 = vi.spyOn(V2ToolPrepareService.prototype, 'prepare'); + + try { + expect(await prepareTools(['ruby'])).toBeUndefined(); + } finally { + await fs.rm(script); + } + + expect(ruby).toHaveBeenCalledOnce(); + expect(v2).not.toHaveBeenCalled(); + }); + test('initializeTools', async () => { expect(await initializeTools(['bun', 'dummy'])).toBeUndefined(); expect(await initializeTools(['not-exist'])).toBeUndefined(); diff --git a/src/cli/prepare-tool/index.ts b/src/cli/prepare-tool/index.ts index 0443a37696..772859b063 100644 --- a/src/cli/prepare-tool/index.ts +++ b/src/cli/prepare-tool/index.ts @@ -26,7 +26,6 @@ import { RubyPrepareService } from '../tools/ruby/index.ts'; import { RustPrepareService } from '../tools/rust.ts'; import { SwiftPrepareService } from '../tools/swift.ts'; import { logger } from '../utils/index.ts'; -import { isNotKnownV2Tool } from '../utils/v2-tool.ts'; import { V2ToolPrepareService } from './prepare-legacy-tools.service.ts'; import { PREPARE_TOOL_TOKEN, @@ -35,7 +34,7 @@ import { /** * Creates a container with all prepare services, including a generic one for - * every v2 shell tool without its own service. + * every v2 shell tool. */ async function prepareContainer(): Promise { logger.trace('preparing container'); @@ -44,18 +43,6 @@ async function prepareContainer(): Promise { // core services container.bind(PrepareToolService).toSelf(); - // v2 tool services - const pathSvc = await container.getAsync(PathService); - const v2Tools = await pathSvc.findLegacyTools(); - for (const tool of v2Tools.filter(isNotKnownV2Tool)) { - @injectable() - @injectFromHierarchy() - class GenericV2ToolPrepareService extends V2ToolPrepareService { - override readonly name: string = tool; - } - container.bind(PREPARE_TOOL_TOKEN).to(GenericV2ToolPrepareService); - } - // modern tool services container.bind(PREPARE_TOOL_TOKEN).to(CabalPrepareService); container.bind(PREPARE_TOOL_TOKEN).to(ConanPrepareService); @@ -81,6 +68,19 @@ async function prepareContainer(): Promise { container.bind(PREPARE_TOOL_TOKEN).to(SbtPrepareService); container.bind(PREPARE_TOOL_TOKEN).to(SwiftPrepareService); + // v2 tool services, after the modern ones so a custom v2 shell tool with the + // same name doesn't replace a modern tool's prepare, like on install + const pathSvc = await container.getAsync(PathService); + const v2Tools = await pathSvc.findLegacyTools(); + for (const tool of v2Tools) { + @injectable() + @injectFromHierarchy() + class GenericV2ToolPrepareService extends V2ToolPrepareService { + override readonly name: string = tool; + } + container.bind(PREPARE_TOOL_TOKEN).to(GenericV2ToolPrepareService); + } + logger.trace('preparing container done'); return container; } diff --git a/src/cli/tools/ruby/index.spec.ts b/src/cli/tools/ruby/index.spec.ts new file mode 100644 index 0000000000..a238244099 --- /dev/null +++ b/src/cli/tools/ruby/index.spec.ts @@ -0,0 +1,235 @@ +import fs from 'node:fs/promises'; +import { arch } from 'node:os'; +import { join } from 'node:path'; +import { codeBlock } from 'common-tags'; +import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; +import { + CompressionService, + EnvService, + LinkToolService, +} from '../../services/index.ts'; +import { getDistro } from '../../utils/index.ts'; +import { RubyInstallService, RubyPrepareService } from './index.ts'; +import { scope } from '~test/http-mock.ts'; +import { ensurePaths } from '~test/path.ts'; +import { checksum, toolContext } from '~test/tool.ts'; + +const { execaMock } = vi.hoisted(() => ({ execaMock: vi.fn() })); +vi.mock('execa', () => ({ execa: execaMock })); +vi.mock('node:os', async (importOriginal) => ({ + ...(await importOriginal()), + arch: vi.fn(() => 'x64'), +})); +vi.mock('../../utils/index.ts', async (importActual) => ({ + ...(await importActual()), + getDistro: vi.fn(), +})); + +const githubUrl = 'https://github.com'; +const archive = 'ruby archive'; + +/** The prebuild download path for `version`, `codename` and `ghArch`. */ +function prebuildPath( + version: string, + codename: string, + ghArch: string, +): string { + return `/containerbase/ruby-prebuild/releases/download/${version}/ruby-${version}-${codename}-${ghArch}.tar.xz`; +} + +describe('cli/tools/ruby/index', () => { + beforeAll(async () => { + await ensurePaths([ + 'tmp/containerbase/cache', + 'home/ubuntu', + 'opt/containerbase/bin', + 'usr/local', + ]); + }); + + beforeEach(() => { + vi.mocked(arch).mockReturnValue('x64'); + vi.mocked(getDistro).mockResolvedValue({ + name: 'Ubuntu', + versionCode: 'jammy', + versionId: '22.04', + }); + // CI configures an apt proxy, which `AptService` would write to `/etc` + vi.stubEnv('APT_HTTP_PROXY', undefined); + execaMock.mockResolvedValue({ failed: false }); + }); + + describe('RubyPrepareService', () => { + test.each(['jammy', 'noble', 'resolute'])('prepare on %s', async (code) => { + vi.mocked(getDistro).mockResolvedValue({ + name: 'Ubuntu', + versionCode: code, + versionId: '24.04', + }); + const { svc, child, pathSvc } = await toolContext(RubyPrepareService); + const envSvc = await child.getAsync(EnvService); + + await expect(svc.prepare()).resolves.toBeUndefined(); + + expect(execaMock).toHaveBeenCalledWith( + 'apt-get', + expect.arrayContaining(['g++', 'libffi-dev', 'libyaml-0-2', 'make']), + { env: { DEBIAN_FRONTEND: 'noninteractive' } }, + ); + for (const entry of ['.gemrc', '.gem', '.cocoapods', 'Library']) { + expect(await fs.readlink(join(envSvc.userHome, entry))).toBe( + join(pathSvc.cachePath, entry), + ); + } + expect(await fs.readlink(join(envSvc.rootDir, 'usr/local/ruby'))).toBe( + pathSvc.toolPath('ruby'), + ); + }); + + test('prepare: throws on an unsupported distro', async () => { + vi.mocked(getDistro).mockResolvedValue({ + name: 'Ubuntu', + versionCode: 'focal', + versionId: '20.04', + }); + const { svc } = await toolContext(RubyPrepareService); + + await expect(svc.prepare()).rejects.toThrow( + "Tool 'ruby' not supported on: focal!", + ); + }); + + test('initialize creates the gemrc and folders', async () => { + const { svc, pathSvc } = await toolContext(RubyPrepareService); + const gemrc = join(pathSvc.cachePath, '.gemrc'); + await fs.rm(gemrc, { force: true }); + + await expect(svc.initialize()).resolves.toBeUndefined(); + + expect(await fs.readFile(gemrc, 'utf8')).toBe('gem: --no-document\n'); + expect((await fs.stat(gemrc)).mode & 0o777).toBe(0o664); + for (const dir of ['.gem', '.cocoapods', 'Library']) { + const stats = await fs.stat(join(pathSvc.cachePath, dir)); + expect(stats.isDirectory()).toBe(true); + expect(stats.mode & 0o777).toBe(0o775); + } + }); + + test('initialize keeps an existing gemrc', async () => { + const { svc, pathSvc } = await toolContext(RubyPrepareService); + const gemrc = join(pathSvc.cachePath, '.gemrc'); + await fs.writeFile(gemrc, '# existing\n'); + + await expect(svc.initialize()).resolves.toBeUndefined(); + + expect(await fs.readFile(gemrc, 'utf8')).toBe('# existing\n'); + }); + }); + + describe('RubyInstallService', () => { + test.each([ + { hostArch: 'x64', ghArch: 'x86_64', version: '3.4.11' }, + { hostArch: 'arm64', ghArch: 'aarch64', version: '4.0.7' }, + ] as const)( + 'install $version on $ghArch with checksum', + async ({ hostArch, ghArch, version }) => { + vi.mocked(arch).mockReturnValue(hostArch); + const { svc, pathSvc } = await toolContext(RubyInstallService); + const path = prebuildPath(version, 'jammy', ghArch); + scope(githubUrl) + .get(`${path}.sha512`) + .reply(200, `${checksum(archive, 'sha512')}\n`) + .get(path) + .reply(200, archive); + const extract = vi.spyOn(CompressionService.prototype, 'extract'); + + await expect(svc.install(version)).resolves.toBeUndefined(); + + expect(extract).toHaveBeenCalledExactlyOnceWith({ + file: expect.stringContaining( + `ruby-${version}-jammy-${ghArch}.tar.xz`, + ), + cwd: pathSvc.toolPath('ruby'), + }); + expect( + await fs.readFile( + join(pathSvc.versionedToolPath('ruby', version), 'etc/gemrc'), + 'utf8', + ), + ).toBe( + `${codeBlock` + gem: --no-document + :benchmark: false + :verbose: true + :update_sources: true + :backtrace: false + `}\n`, + ); + }, + ); + + test('install on noble uses the jammy prebuild', async () => { + vi.mocked(getDistro).mockResolvedValue({ + name: 'Ubuntu', + versionCode: 'noble', + versionId: '24.04', + }); + const version = '3.3.0'; + const { svc, pathSvc } = await toolContext(RubyInstallService); + const path = prebuildPath(version, 'jammy', 'x86_64'); + scope(githubUrl) + .get(`${path}.sha512`) + .reply(200, `${checksum(archive, 'sha512')}\n`) + .get(path) + .reply(200, archive); + const extract = vi.spyOn(CompressionService.prototype, 'extract'); + + await expect(svc.install(version)).resolves.toBeUndefined(); + + expect(extract).toHaveBeenCalledExactlyOnceWith({ + file: expect.stringContaining(`ruby-${version}-jammy-x86_64.tar.xz`), + cwd: pathSvc.toolPath('ruby'), + }); + expect( + await fs.readFile( + join(pathSvc.versionedToolPath('ruby', version), 'etc/gemrc'), + 'utf8', + ), + ).toContain(':backtrace: false'); + }); + + test('link', async () => { + const { svc, pathSvc } = await toolContext(RubyInstallService); + const spy = vi.spyOn(LinkToolService.prototype, 'shellwrapper'); + const src = join(pathSvc.versionedToolPath('ruby', '3.4.11'), 'bin'); + + await expect(svc.link('3.4.11')).resolves.toBeUndefined(); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith('ruby', { srcDir: src }); + expect(spy).toHaveBeenCalledWith('ruby', { srcDir: src, name: 'gem' }); + }); + + test('runs the tool test', async () => { + const { svc } = await toolContext(RubyInstallService); + + await expect(svc.test('3.4.11')).resolves.toBeUndefined(); + + expect(execaMock).toHaveBeenCalledWith( + 'ruby', + ['--version'], + expect.any(Object), + ); + expect(execaMock).toHaveBeenCalledWith( + 'gem', + ['--version'], + expect.any(Object), + ); + expect(execaMock).toHaveBeenCalledWith( + 'gem', + ['env'], + expect.any(Object), + ); + }); + }); +}); diff --git a/src/cli/tools/ruby/index.ts b/src/cli/tools/ruby/index.ts index cc129e1dae..d90be3a7d2 100644 --- a/src/cli/tools/ruby/index.ts +++ b/src/cli/tools/ruby/index.ts @@ -1,18 +1,116 @@ -import { injectFromHierarchy, injectable } from 'inversify'; -import { V2ToolInstallService } from '../../install-tool/install-legacy-tool.service.ts'; -import { V2ToolPrepareService } from '../../prepare-tool/prepare-legacy-tools.service.ts'; -import { v2Tool } from '../../utils/v2-tool.ts'; +import { join } from 'node:path'; +import { inject, injectFromHierarchy, injectable } from 'inversify'; +import { BasePrepareService } from '../../prepare-tool/base-prepare.service.ts'; +import { AptService } from '../../services/index.ts'; +import { fileContent, getDistro } from '../../utils/index.ts'; +import { PrebuildInstallService } from '../utils/prebuild.ts'; + +/** The ubuntu releases ruby can be installed on. */ +const supportedDistros = ['jammy', 'noble', 'resolute']; + +/** The user home entries redirected into the containerbase cache. */ +const cacheDirs = ['.gem', '.cocoapods', 'Library']; @injectable() @injectFromHierarchy() -@v2Tool('ruby') -export class RubyPrepareService extends V2ToolPrepareService { +export class RubyPrepareService extends BasePrepareService { + @inject(AptService) + private readonly aptSvc!: AptService; + override readonly name = 'ruby'; + + /** + * Installs the apt packages ruby needs, initializes the cache, links the + * user's gem and cocoapods folders to it, and links `/usr/local/ruby` to the + * tool path, a workaround for ruby's hard-coded paths. + * + * @throws on an unsupported distro + */ + override async prepare(): Promise { + const { versionCode } = await getDistro(); + if (!supportedDistros.includes(versionCode)) { + throw new Error( + `Tool '${this.name}' not supported on: ${versionCode}! Please use ubuntu 'jammy', 'noble' or 'resolute'.`, + ); + } + + await this.aptSvc.install('g++', 'libffi-dev', 'libyaml-0-2', 'make'); + + await this.initialize(); + + for (const entry of ['.gemrc', ...cacheDirs]) { + await this.pathSvc.createSymlink( + join(this.pathSvc.cachePath, entry), + join(this.envSvc.userHome, entry), + ); + } + + await this.pathSvc.createSymlink( + this.pathSvc.toolPath(this.name), + join(this.envSvc.rootDir, 'usr/local/ruby'), + ); + } + + /** + * Creates the `.gemrc` and the gem, cocoapods and `Library` folders in the + * containerbase cache, unless the `.gemrc` already exists. + */ + override async initialize(): Promise { + const gemrc = join(this.pathSvc.cachePath, '.gemrc'); + if (await this.pathSvc.fileExists(gemrc)) { + return; + } + + await this.pathSvc.writeFile( + gemrc, + fileContent` + gem: --no-document + `, + ); + + for (const dir of cacheDirs) { + await this.pathSvc.createDir(join(this.pathSvc.cachePath, dir)); + } + } } @injectable() @injectFromHierarchy() -@v2Tool('ruby') -export class RubyInstallService extends V2ToolInstallService { +export class RubyInstallService extends PrebuildInstallService { override readonly name = 'ruby'; + + /** Installs the ruby prebuild and writes its system wide `gemrc`. */ + override async install(version: string): Promise { + await super.install(version); + + const etc = await this.pathSvc.createVersionedToolPath( + this.name, + version, + 'etc', + ); + await this.pathSvc.writeFile( + join(etc, 'gemrc'), + fileContent` + gem: --no-document + :benchmark: false + :verbose: true + :update_sources: true + :backtrace: false + `, + ); + } + + /** Links the `ruby` and `gem` binaries into the global bin folder. */ + override async link(version: string): Promise { + await super.link(version); + const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin'); + await this.shellwrapper({ srcDir: src, name: 'gem' }); + } + + /** Checks that `ruby` and `gem` run and prints the gem environment. */ + override async test(version: string): Promise { + await super.test(version); + await this._spawn('gem', ['--version']); + await this._spawn('gem', ['env']); + } } diff --git a/src/cli/utils/types.ts b/src/cli/utils/types.ts index 6b345378d9..1223c958fa 100644 --- a/src/cli/utils/types.ts +++ b/src/cli/utils/types.ts @@ -21,8 +21,6 @@ export type CliMode = (typeof cliModes)[number]; export type Arch = 'arm64' | 'amd64'; -export type ClazzDecorator = (target: V) => V | void; - export const installToolTypes = ['gem', 'npm', 'pip'] as const; export type InstallToolType = (typeof installToolTypes)[number]; diff --git a/src/cli/utils/v2-tool.spec.ts b/src/cli/utils/v2-tool.spec.ts deleted file mode 100644 index e8b4b830f0..0000000000 --- a/src/cli/utils/v2-tool.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { isKnownV2Tool, isNotKnownV2Tool, v2Tool } from './v2-tool.ts'; - -describe('cli/utils/v2-tool', () => { - @v2Tool('test-tool') - class TestTool { - readonly name = 'test-tool'; - } - - const tool = new TestTool().name; - - test('isKnownV2Tool', () => { - expect(isKnownV2Tool(tool)).toBe(true); - }); - - test('isNotKnownV2Tool', () => { - expect(isNotKnownV2Tool(tool)).toBe(false); - }); -}); diff --git a/src/cli/utils/v2-tool.ts b/src/cli/utils/v2-tool.ts deleted file mode 100644 index cfd9f33951..0000000000 --- a/src/cli/utils/v2-tool.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ClazzDecorator } from './types'; - -const knownV2Tools = new Set(); - -/** Whether a v2 shell tool has its own service, see `v2Tool`. */ -export function isKnownV2Tool(tool: string): boolean { - return knownV2Tools.has(tool); -} - -/** Whether a v2 shell tool has no own service and needs a generic one. */ -export function isNotKnownV2Tool(tool: string): boolean { - return !knownV2Tools.has(tool); -} - -interface V2ToolInstallerService { - prototype: { name: string }; -} - -/** - * Class decorator which marks a v2 shell tool as having its own service, so - * no generic one is registered for it. - */ -export function v2Tool(tool: string): ClazzDecorator { - return (target: T): T | void => { - knownV2Tools.add(tool); - - return target; - }; -} diff --git a/src/usr/local/containerbase/tools/v2/ruby.sh b/src/usr/local/containerbase/tools/v2/ruby.sh deleted file mode 100644 index bb93f45ace..0000000000 --- a/src/usr/local/containerbase/tools/v2/ruby.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/bin/bash - -function prepare_tool() { - local version_codename - local path - - version_codename="$(get_distro)" - case "${version_codename}" in - "jammy");; - "noble");; - "resolute");; - *) - echo "Tool '${TOOL_NAME}' not supported on: ${version_codename}! Please use ubuntu 'noble' or 'resolute'." >&2 - exit 1 - ;; - esac - apt_install \ - g++ \ - libffi-dev \ - libyaml-0-2 \ - make \ - ; - - init_tool - - # Redirect gemrc - path="$(get_cache_path)/.gemrc" - ln -sf "${path}" "${USER_HOME}/.gemrc" - - # Redirect gem home - path="$(get_cache_path)/.gem" - ln -sf "${path}" "${USER_HOME}/.gem" - - # Redirect cocoapods home - path="$(get_cache_path)/.cocoapods" - ln -sf "${path}" "${USER_HOME}/.cocoapods" - - # Redirect Library home - path="$(get_cache_path)/Library" - ln -sf "${path}" "${USER_HOME}/Library" - - # Workaround for compatibillity for Ruby hardcoded paths - path=$(find_tool_path) - if [ "${path}" != "${ROOT_DIR_LEGACY}/ruby" ]; then - ln -sf "${path}" /usr/local/ruby - fi -} - -function init_tool () { - local path - path="$(get_cache_path)/.gemrc" - - if [ -f "${path}" ]; then - return - fi - - # Init gemrc - { - printf -- "gem: --no-document\n" - } > "${path}" - chown "${USER_ID}" "${path}" - chmod g+w "${path}" - - # Init gem home - path="$(get_cache_path)/.gem" - create_folder "${path}" 775 - chown "${USER_ID}" "${path}" - - # Init cocoapods home - path="$(get_cache_path)/.cocoapods" - create_folder "${path}" 775 - chown "${USER_ID}" "${path}" - - # Init Library home - path="$(get_cache_path)/Library" - create_folder "${path}" 775 - chown "${USER_ID}" "${path}" -} - -function install_tool () { - local arch=${ARCHITECTURE} - local base_url - local checksum_file - local expected_checksum - local file - local name=${TOOL_NAME} - local tool_path - local version=${TOOL_VERSION} - local version_codename - local versioned_tool_path - - tool_path=$(find_tool_path) - - base_url="https://github.com/containerbase/${name}-prebuild/releases/download" - version_codename=$(get_distro) - - if [[ "${version_codename}" == "noble" || "${version_codename}" == "resolute" ]]; then - version_codename="jammy" - fi - - # not all releases have checksums - checksum_exists=$(file_exists "${base_url}/${version}/${name}-${version}-${version_codename}-${arch}.tar.xz.sha512") - if [[ "${checksum_exists}" == "200" ]]; then - checksum_file=$(get_from_url "${base_url}/${version}/${name}-${version}-${version_codename}-${arch}.tar.xz.sha512") - # get checksum from file - expected_checksum=$(cat "${checksum_file}") - fi - - file=$(get_from_url \ - "${base_url}/${version}/${name}-${version}-${version_codename}-${arch}.tar.xz" \ - "${name}-${version}-${version_codename}-${arch}.tar.xz" \ - "${expected_checksum}" \ - sha512sum - ) - - bsdtar -C "${tool_path}" -xf "${file}" - - versioned_tool_path=$(find_versioned_tool_path) - # System settings - mkdir -p "$versioned_tool_path/etc" - { - printf -- "gem: --no-document\n" - printf -- ":benchmark: false\n" - printf -- ":verbose: true\n" - printf -- ":update_sources: true\n" - printf -- ":backtrace: false\n" - } > "$versioned_tool_path/etc/gemrc" - -} - -function link_tool () { - local versioned_tool_path - - versioned_tool_path=$(find_versioned_tool_path) - - shell_wrapper ruby "${versioned_tool_path}/bin" - shell_wrapper gem "${versioned_tool_path}/bin" -} - -function test_tool () { - ruby --version - echo "gem $(gem --version)" - gem env -} diff --git a/test/latest/Dockerfile b/test/latest/Dockerfile index afdbf5fed1..cbbaf68b81 100644 --- a/test/latest/Dockerfile +++ b/test/latest/Dockerfile @@ -386,6 +386,38 @@ RUN install-tool nuget 7.9.0 RUN nuget search nuget.commandline -Take 1 +#-------------------------------------- +# test: v1 and v2 shell tools of custom images +#-------------------------------------- +FROM base AS test-legacy + +COPY test/latest/legacy-tools/ /usr/local/containerbase/tools/ + +RUN install-tool dummy-v1 1.2.3 + +RUN prepare-tool dummy-v2 +RUN set -ex; [ -f /opt/containerbase/tools/dummy-v2/prepared ] + +RUN install-tool dummy-v2 1.2.3 +RUN set -ex; [ -f /opt/containerbase/tools/dummy-v2/initialized ] + +RUN set -ex; \ + dummy-v1 --version | grep '^dummy-v1 1.2.3$'; \ + dummy-v2 --version | grep '^dummy-v2 1.2.3$'; \ + containerbase-cli list tools | grep -E '^dummy-v1 +1\.2\.3$'; \ + containerbase-cli list tools | grep -E '^dummy-v2 +1\.2\.3$'; \ + true + +USER 12021 + +RUN install-tool dummy-v2 2.0.0 + +RUN set -ex; \ + dummy-v1 --version | grep '^dummy-v1 1.2.3$'; \ + dummy-v2 --version | grep '^dummy-v2 2.0.0$'; \ + containerbase-cli list tools | grep -E '^dummy-v2 +2\.0\.0 +1\.2\.3$'; \ + true + #-------------------------------------- # final #-------------------------------------- @@ -399,3 +431,4 @@ COPY --link --from=teste /.dummy /.dummy COPY --link --from=testf /.dummy /.dummy COPY --link --from=test-docker /.dummy /.dummy COPY --link --from=test-mono /.dummy /.dummy +COPY --link --from=test-legacy /.dummy /.dummy diff --git a/test/latest/Dockerfile.arm64 b/test/latest/Dockerfile.arm64 index 014fe02642..720e53bc41 100644 --- a/test/latest/Dockerfile.arm64 +++ b/test/latest/Dockerfile.arm64 @@ -213,6 +213,38 @@ RUN install-tool protoc 36.2 # renovate: datasource=github-releases packageName=prefix-dev/pixi RUN install-tool pixi v0.81.0 +#-------------------------------------- +# Image: v1 and v2 shell tools of custom images +#-------------------------------------- +FROM base AS test-legacy + +COPY test/latest/legacy-tools/ /usr/local/containerbase/tools/ + +RUN install-tool dummy-v1 1.2.3 + +RUN prepare-tool dummy-v2 +RUN set -ex; [ -f /opt/containerbase/tools/dummy-v2/prepared ] + +RUN install-tool dummy-v2 1.2.3 +RUN set -ex; [ -f /opt/containerbase/tools/dummy-v2/initialized ] + +RUN set -ex; \ + dummy-v1 --version | grep '^dummy-v1 1.2.3$'; \ + dummy-v2 --version | grep '^dummy-v2 1.2.3$'; \ + containerbase-cli list tools | grep -E '^dummy-v1 +1\.2\.3$'; \ + containerbase-cli list tools | grep -E '^dummy-v2 +1\.2\.3$'; \ + true + +USER 12021 + +RUN install-tool dummy-v2 2.0.0 + +RUN set -ex; \ + dummy-v1 --version | grep '^dummy-v1 1.2.3$'; \ + dummy-v2 --version | grep '^dummy-v2 2.0.0$'; \ + containerbase-cli list tools | grep -E '^dummy-v2 +2\.0\.0 +1\.2\.3$'; \ + true + #-------------------------------------- # Image: final #-------------------------------------- @@ -237,3 +269,4 @@ COPY --from=test-tofu /.dummy /.dummy COPY --from=test-vendir /.dummy /.dummy COPY --from=test-others /.dummy /.dummy +COPY --from=test-legacy /.dummy /.dummy diff --git a/test/latest/legacy-tools/dummy-v1.sh b/test/latest/legacy-tools/dummy-v1.sh new file mode 100644 index 0000000000..74d2fe8348 --- /dev/null +++ b/test/latest/legacy-tools/dummy-v1.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# v1 shell tool like custom images ship them, sourced by `v1-install-tool.sh` +# installs a small script which prints its name and version + +require_root + +versioned_tool_path=$(create_versioned_tool_path) +create_folder "${versioned_tool_path}/bin" +printf '#!/bin/bash\necho "%s %s"\n' "${TOOL_NAME}" "${TOOL_VERSION}" > "${versioned_tool_path}/bin/${TOOL_NAME}" +chmod +x "${versioned_tool_path}/bin/${TOOL_NAME}" + +shell_wrapper "${TOOL_NAME}" "${versioned_tool_path}/bin" + +[[ -n $SKIP_VERSION ]] || dummy-v1 --version diff --git a/test/latest/legacy-tools/v2/dummy-v2.sh b/test/latest/legacy-tools/v2/dummy-v2.sh new file mode 100644 index 0000000000..bc469abb41 --- /dev/null +++ b/test/latest/legacy-tools/v2/dummy-v2.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# v2 shell tool like custom images ship them, sourced by `v2-install-tool.sh` +# installs a small script which prints its name and version + +function prepare_tool() { + touch "$(find_tool_path)/prepared" +} + +function init_tool () { + touch "$(find_tool_path)/initialized" +} + +function install_tool () { + local versioned_tool_path + + versioned_tool_path=$(create_versioned_tool_path) + create_folder "${versioned_tool_path}/bin" + printf '#!/bin/bash\necho "%s %s"\n' "${TOOL_NAME}" "${TOOL_VERSION}" > "${versioned_tool_path}/bin/${TOOL_NAME}" + chmod +x "${versioned_tool_path}/bin/${TOOL_NAME}" +} + +function link_tool () { + shell_wrapper "${TOOL_NAME}" "$(find_versioned_tool_path)/bin" +} + +function test_tool () { + dummy-v2 --version +}