From 094239d9ebc4ae4ea8b01ea3ec9a1bf512531601 Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Fri, 17 Feb 2023 21:56:41 +0100 Subject: [PATCH] buildx: build buildx as install method Signed-off-by: CrazyMax --- __tests__/buildx/install.test.ts | 22 ++++++-- src/buildx/install.ts | 93 +++++++++++++++++++++++++++++--- 2 files changed, 106 insertions(+), 9 deletions(-) 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/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);