diff --git a/__tests__/buildx/buildx.test.ts b/__tests__/buildx/buildx.test.ts index cdf98c4..b56bd70 100644 --- a/__tests__/buildx/buildx.test.ts +++ b/__tests__/buildx/buildx.test.ts @@ -88,14 +88,6 @@ describe('certsDir', () => { }); }); -describe('install', () => { - it('acquires buildx v0.9.1', async () => { - const buildx = new Buildx({context: new Context()}); - const buildxBin = await buildx.install('v0.9.1', tmpDir); - expect(fs.existsSync(buildxBin)).toBe(true); - }, 100000); -}); - describe('isAvailable', () => { it('docker cli', async () => { const execSpy = jest.spyOn(exec, 'getExecOutput'); diff --git a/__tests__/buildx/install.test.ts b/__tests__/buildx/install.test.ts index 776a129..d553b65 100644 --- a/__tests__/buildx/install.test.ts +++ b/__tests__/buildx/install.test.ts @@ -33,7 +33,7 @@ afterEach(function () { rimraf.sync(tmpDir); }); -describe('install', () => { +describe('download', () => { // prettier-ignore test.each([ ['v0.9.1', false], @@ -43,7 +43,7 @@ describe('install', () => { ])( 'acquires %p of buildx (standalone: %p)', async (version, standalone) => { const install = new Install({standalone: standalone}); - const buildxBin = await install.install(version, tmpDir); + const buildxBin = await install.download(version, tmpDir); expect(fs.existsSync(buildxBin)).toBe(true); }, 100000 @@ -65,7 +65,7 @@ describe('install', () => { jest.spyOn(osm, 'platform').mockImplementation(() => os); jest.spyOn(osm, 'arch').mockImplementation(() => arch); const install = new Install(); - const buildxBin = await install.install('latest', tmpDir); + const buildxBin = await install.download('latest', tmpDir); expect(fs.existsSync(buildxBin)).toBe(true); }, 100000 @@ -78,6 +78,22 @@ describe('install', () => { }); }); +describe('build', () => { + // eslint-disable-next-line jest/no-disabled-tests + it.skip('builds refs/pull/648/head', async () => { + const install = new Install(); + const buildxBin = await install.build('https://github.com/docker/buildx.git#refs/pull/648/head', tmpDir); + expect(fs.existsSync(buildxBin)).toBe(true); + }, 100000); + + // eslint-disable-next-line jest/no-disabled-tests + it.skip('builds 67bd6f4dc82a9cd96f34133dab3f6f7af803bb14', async () => { + const install = new Install(); + const buildxBin = await install.build('https://github.com/docker/buildx.git#67bd6f4dc82a9cd96f34133dab3f6f7af803bb14', tmpDir); + expect(fs.existsSync(buildxBin)).toBe(true); + }, 100000); +}); + describe('getRelease', () => { it('returns latest buildx GitHub release', async () => { const release = await Install.getRelease('latest'); diff --git a/src/buildx/buildx.ts b/src/buildx/buildx.ts index 6f8fb8e..eaf7765 100644 --- a/src/buildx/buildx.ts +++ b/src/buildx/buildx.ts @@ -22,7 +22,6 @@ import * as semver from 'semver'; import {Docker} from '../docker'; import {Context} from '../context'; import {Inputs} from './inputs'; -import {Install} from './install'; import {Cert} from '../types/buildx'; @@ -34,7 +33,6 @@ export interface BuildxOpts { export class Buildx { private readonly context: Context; private _version: string | undefined; - private _install: Install; public readonly inputs: Inputs; public readonly standalone: boolean; @@ -43,7 +41,6 @@ export class Buildx { this.context = opts.context; this.inputs = new Inputs(this.context); this.standalone = opts?.standalone ?? !Docker.isAvailable; - this._install = new Install({standalone: opts.standalone}); } static get configDir(): string { @@ -61,10 +58,6 @@ export class Buildx { }; } - public async install(version: string, dest: string): Promise { - return await this._install.install(version, dest); - } - public async isAvailable(): Promise { const cmd = this.getCommand([]); return await exec diff --git a/src/buildx/install.ts b/src/buildx/install.ts index 10b4f39..b416967 100644 --- a/src/buildx/install.ts +++ b/src/buildx/install.ts @@ -18,27 +18,37 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import * as core from '@actions/core'; +import * as exec from '@actions/exec'; import * as httpm from '@actions/http-client'; import * as tc from '@actions/tool-cache'; import * as semver from 'semver'; import * as util from 'util'; +import {Buildx} from './buildx'; +import {Context} from '../context'; +import {Docker} from '../docker'; +import {Git} from '../git'; + import {GitHubRelease} from '../types/github'; export interface InstallOpts { + context?: Context; standalone?: boolean; } export class Install { - private readonly opts: InstallOpts; + private readonly context: Context; + private readonly standalone: boolean; constructor(opts?: InstallOpts) { - this.opts = opts || {}; + this.context = opts?.context || new Context(); + this.standalone = opts?.standalone ?? !Docker.isAvailable; } - public async install(version: string, dest: string): Promise { + public async download(version: string, dest: string): Promise { const release: GitHubRelease = await Install.getRelease(version); const fversion = release.tag_name.replace(/^v+|v+$/g, ''); + let toolPath: string; toolPath = tc.find('buildx', fversion, this.platform()); if (!toolPath) { @@ -46,14 +56,85 @@ export class Install { if (!semver.valid(c)) { throw new Error(`Invalid Buildx version "${fversion}".`); } - toolPath = await this.download(fversion); + toolPath = await this.fetchBinary(fversion); } - if (this.opts.standalone) { + + if (this.standalone) { return this.setStandalone(toolPath, dest); } return this.setPlugin(toolPath, dest); } + public async build(gitContext: string, dest: string): Promise { + // eslint-disable-next-line prefer-const + let [repo, ref] = gitContext.split('#'); + if (ref.length == 0) { + ref = 'master'; + } + + let vspec: string; + // TODO: include full ref as fingerprint. Use commit sha as best-effort in the meantime. + if (ref.match(/^[0-9a-fA-F]{40}$/)) { + vspec = ref; + } else { + vspec = await Git.getRemoteSha(repo, ref); + } + core.debug(`Tool version spec ${vspec}`); + + let toolPath: string; + toolPath = tc.find('buildx', vspec); + if (!toolPath) { + const outputDir = path.join(this.context.tmpDir(), 'build-cache').split(path.sep).join(path.posix.sep); + const buildCmd = await this.buildCommand(gitContext, outputDir); + toolPath = await exec + .getExecOutput(buildCmd.command, buildCmd.args, { + ignoreReturnCode: true + }) + .then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + core.warning(res.stderr.trim()); + } + return tc.cacheFile(`${outputDir}/buildx`, os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx', 'buildx', vspec); + }); + } + + if (this.standalone) { + return this.setStandalone(toolPath, dest); + } + return this.setPlugin(toolPath, dest); + } + + private async buildCommand(gitContext: string, outputDir: string): Promise<{args: Array; command: string}> { + const buildxStandaloneFound = await new Buildx({context: this.context, standalone: true}).isAvailable(); + const buildxPluginFound = await new Buildx({context: this.context, standalone: false}).isAvailable(); + + let buildStandalone = false; + if (this.standalone && buildxStandaloneFound) { + core.debug(`Buildx standalone found, build with it`); + buildStandalone = true; + } else if (!this.standalone && buildxPluginFound) { + core.debug(`Buildx plugin found, build with it`); + buildStandalone = false; + } else if (buildxStandaloneFound) { + core.debug(`Buildx plugin not found, but standalone found so trying to build with it`); + buildStandalone = true; + } else if (buildxPluginFound) { + core.debug(`Buildx standalone not found, but plugin found so trying to build with it`); + buildStandalone = false; + } else { + throw new Error(`Neither buildx standalone or plugin have been found to build from ref ${gitContext}`); + } + + //prettier-ignore + return new Buildx({context: this.context, standalone: buildStandalone}).getCommand([ + 'build', + '--target', 'binaries', + '--build-arg', 'BUILDKIT_CONTEXT_KEEP_GIT_DIR=1', + '--output', `type=local,dest=${outputDir}`, + gitContext + ]); + } + private async setStandalone(toolPath: string, dest: string): Promise { const toolBinPath = path.join(toolPath, os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx'); const binDir = path.join(dest, 'bin'); @@ -81,7 +162,7 @@ export class Install { return pluginPath; } - private async download(version: string): Promise { + private async fetchBinary(version: string): Promise { const targetFile: string = os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx'; const downloadURL = util.format('https://github.com/docker/buildx/releases/download/v%s/%s', version, this.filename(version)); const downloadPath = await tc.downloadTool(downloadURL); diff --git a/src/toolkit.ts b/src/toolkit.ts index 9d189d2..dbe03be 100644 --- a/src/toolkit.ts +++ b/src/toolkit.ts @@ -16,6 +16,7 @@ import {Context} from './context'; import {Buildx} from './buildx/buildx'; +import {Install} from './buildx/install'; import {BuildKit} from './buildkit/buildkit'; import {GitHub} from './github'; @@ -31,12 +32,14 @@ export class Toolkit { public context: Context; public github: GitHub; public buildx: Buildx; + public buildxInstall: Install; public buildkit: BuildKit; constructor(opts: ToolkitOpts = {}) { this.context = new Context(); this.github = new GitHub({token: opts.githubToken}); this.buildx = new Buildx({context: this.context}); + this.buildxInstall = new Install({context: this.context, standalone: this.buildx.standalone}); this.buildkit = new BuildKit({context: this.context, buildx: this.buildx}); } }