setup-buildx-action/src/buildx.ts
CrazyMax 3472856dd9
support standalone mode and display version
Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
2022-04-17 17:22:11 +02:00

338 lines
11 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
import * as semver from 'semver';
import * as util from 'util';
import * as context from './context';
import * as git from './git';
import * as github from './github';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import * as tc from '@actions/tool-cache';
export type Builder = {
name?: string;
driver?: string;
node_name?: string;
node_endpoint?: string;
node_status?: string;
node_flags?: string;
node_platforms?: string;
};
export async function getConfigInline(s: string): Promise<string> {
return getConfig(s, false);
}
export async function getConfigFile(s: string): Promise<string> {
return getConfig(s, true);
}
export async function getConfig(s: string, file: boolean): Promise<string> {
if (file) {
if (!fs.existsSync(s)) {
throw new Error(`config file ${s} not found`);
}
s = fs.readFileSync(s, {encoding: 'utf-8'});
}
const configFile = context.tmpNameSync({
tmpdir: context.tmpDir()
});
fs.writeFileSync(configFile, s);
return configFile;
}
export async function isAvailable(standalone?: boolean): Promise<boolean> {
const cmd = getCommand([], standalone);
return await exec
.getExecOutput(cmd.commandLine, cmd.args, {
ignoreReturnCode: true,
silent: true
})
.then(res => {
if (res.stderr.length > 0 && res.exitCode != 0) {
return false;
}
return res.exitCode == 0;
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.catch(error => {
return false;
});
}
export async function getVersion(standalone?: boolean): Promise<string> {
const cmd = getCommand(['version'], standalone);
return await exec
.getExecOutput(cmd.commandLine, cmd.args, {
ignoreReturnCode: true,
silent: true
})
.then(res => {
if (res.stderr.length > 0 && res.exitCode != 0) {
throw new Error(res.stderr.trim());
}
return parseVersion(res.stdout.trim());
});
}
export function parseVersion(stdout: string): string {
const matches = /\sv?([0-9a-f]{7}|[0-9.]+)/.exec(stdout);
if (!matches) {
throw new Error(`Cannot parse buildx version`);
}
return matches[1];
}
export function satisfies(version: string, range: string): boolean {
return semver.satisfies(version, range) || /^[0-9a-f]{7}$/.exec(version) !== null;
}
export async function inspect(name: string, standalone?: boolean): Promise<Builder> {
const cmd = getCommand(['inspect', name], standalone);
return await exec
.getExecOutput(cmd.commandLine, cmd.args, {
ignoreReturnCode: true,
silent: true
})
.then(res => {
if (res.stderr.length > 0 && res.exitCode != 0) {
throw new Error(res.stderr.trim());
}
const builder: Builder = {};
itlines: for (const line of res.stdout.trim().split(`\n`)) {
const [key, ...rest] = line.split(':');
const value = rest.map(v => v.trim()).join(':');
if (key.length == 0 || value.length == 0) {
continue;
}
switch (key) {
case 'Name': {
if (builder.name == undefined) {
builder.name = value;
} else {
builder.node_name = value;
}
break;
}
case 'Driver': {
builder.driver = value;
break;
}
case 'Endpoint': {
builder.node_endpoint = value;
break;
}
case 'Status': {
builder.node_status = value;
break;
}
case 'Flags': {
builder.node_flags = value;
break;
}
case 'Platforms': {
builder.node_platforms = value.replace(/\s/g, '');
break itlines;
}
}
}
return builder;
});
}
export async function build(inputBuildRef: string, dest: string, standalone: boolean): Promise<string> {
// eslint-disable-next-line prefer-const
let [repo, ref] = inputBuildRef.split('#');
if (ref.length == 0) {
ref = 'master';
}
let vspec: string;
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 outFolder = path.join(context.tmpDir(), 'out').split(path.sep).join(path.posix.sep);
let buildWithStandalone = false;
const standaloneFound = await isAvailable(true);
const pluginFound = await isAvailable(false);
if (standalone && standaloneFound) {
core.debug(`Buildx standalone found, build with it`);
buildWithStandalone = true;
} else if (!standalone && pluginFound) {
core.debug(`Buildx plugin found, build with it`);
buildWithStandalone = false;
} else if (standaloneFound) {
core.debug(`Buildx plugin not found, but standalone found so trying to build with it`);
buildWithStandalone = true;
} else if (pluginFound) {
core.debug(`Buildx standalone not found, but plugin found so trying to build with it`);
buildWithStandalone = false;
} else {
throw new Error(`Neither buildx standalone or plugin have been found to build from ref`);
}
const buildCmd = getCommand(['build', '--target', 'binaries', '--build-arg', 'BUILDKIT_CONTEXT_KEEP_GIT_DIR=1', '--output', `type=local,dest=${outFolder}`, inputBuildRef], buildWithStandalone);
toolPath = await exec
.getExecOutput(buildCmd.commandLine, buildCmd.args, {
ignoreReturnCode: true
})
.then(res => {
if (res.stderr.length > 0 && res.exitCode != 0) {
core.warning(res.stderr.trim());
}
return tc.cacheFile(`${outFolder}/buildx`, context.osPlat == 'win32' ? 'docker-buildx.exe' : 'docker-buildx', 'buildx', vspec);
});
}
if (standalone) {
return setStandalone(toolPath, dest);
}
return setPlugin(toolPath, dest);
}
export async function install(inputVersion: string, dest: string, standalone: boolean): Promise<string> {
const release: github.GitHubRelease | null = await github.getRelease(inputVersion);
if (!release) {
throw new Error(`Cannot find buildx ${inputVersion} release`);
}
core.debug(`Release ${release.tag_name} found`);
const version = release.tag_name.replace(/^v+|v+$/g, '');
let toolPath: string;
toolPath = tc.find('buildx', version);
if (!toolPath) {
const c = semver.clean(version) || '';
if (!semver.valid(c)) {
throw new Error(`Invalid Buildx version "${version}".`);
}
toolPath = await download(version);
}
if (standalone) {
return setStandalone(toolPath, dest);
}
return setPlugin(toolPath, dest);
}
async function setStandalone(toolPath: string, dest: string): Promise<string> {
core.info('Standalone mode');
const toolBinPath = path.join(toolPath, context.osPlat == 'win32' ? 'docker-buildx.exe' : 'docker-buildx');
const binDir = path.join(dest, 'bin');
core.debug(`Bin dir is ${binDir}`);
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, {recursive: true});
}
const filename: string = context.osPlat == 'win32' ? 'buildx.exe' : 'buildx';
const buildxPath: string = path.join(binDir, filename);
core.debug(`Bin path is ${buildxPath}`);
fs.copyFileSync(toolBinPath, buildxPath);
core.info('Fixing perms');
fs.chmodSync(buildxPath, '0755');
core.addPath(binDir);
core.info('Added buildx to the path');
return buildxPath;
}
async function setPlugin(toolPath: string, dockerConfigHome: string): Promise<string> {
core.info('Docker plugin mode');
const toolBinPath = path.join(toolPath, context.osPlat == 'win32' ? 'docker-buildx.exe' : 'docker-buildx');
const pluginsDir: string = path.join(dockerConfigHome, 'cli-plugins');
core.debug(`Plugins dir is ${pluginsDir}`);
if (!fs.existsSync(pluginsDir)) {
fs.mkdirSync(pluginsDir, {recursive: true});
}
const filename: string = context.osPlat == 'win32' ? 'docker-buildx.exe' : 'docker-buildx';
const pluginPath: string = path.join(pluginsDir, filename);
core.debug(`Plugin path is ${pluginPath}`);
fs.copyFileSync(toolBinPath, pluginPath);
core.info('Fixing perms');
fs.chmodSync(pluginPath, '0755');
return pluginPath;
}
async function download(version: string): Promise<string> {
const targetFile: string = context.osPlat == 'win32' ? 'docker-buildx.exe' : 'docker-buildx';
const downloadUrl = util.format('https://github.com/docker/buildx/releases/download/v%s/%s', version, await filename(version));
core.info(`Downloading ${downloadUrl}`);
const downloadPath = await tc.downloadTool(downloadUrl);
core.debug(`Downloaded to ${downloadPath}`);
return await tc.cacheFile(downloadPath, targetFile, 'buildx', version);
}
async function filename(version: string): Promise<string> {
let arch: string;
switch (context.osArch) {
case 'x64': {
arch = 'amd64';
break;
}
case 'ppc64': {
arch = 'ppc64le';
break;
}
case 'arm': {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const arm_version = (process.config.variables as any).arm_version;
arch = arm_version ? 'arm-v' + arm_version : 'arm';
break;
}
default: {
arch = context.osArch;
break;
}
}
const platform: string = context.osPlat == 'win32' ? 'windows' : context.osPlat;
const ext: string = context.osPlat == 'win32' ? '.exe' : '';
return util.format('buildx-v%s.%s-%s%s', version, platform, arch, ext);
}
export async function getBuildKitVersion(containerID: string): Promise<string> {
return exec
.getExecOutput(`docker`, ['inspect', '--format', '{{.Config.Image}}', containerID], {
ignoreReturnCode: true,
silent: true
})
.then(bkitimage => {
if (bkitimage.exitCode == 0 && bkitimage.stdout.length > 0) {
return exec
.getExecOutput(`docker`, ['run', '--rm', bkitimage.stdout.trim(), '--version'], {
ignoreReturnCode: true,
silent: true
})
.then(bkitversion => {
if (bkitversion.exitCode == 0 && bkitversion.stdout.length > 0) {
return `${bkitimage.stdout.trim()} => ${bkitversion.stdout.trim()}`;
} else if (bkitversion.stderr.length > 0) {
core.warning(bkitversion.stderr.trim());
}
return bkitversion.stdout.trim();
});
} else if (bkitimage.stderr.length > 0) {
core.warning(bkitimage.stderr.trim());
}
return bkitimage.stdout.trim();
});
}
export function getCommand(args: Array<string>, standalone?: boolean) {
return {
commandLine: standalone ? 'buildx' : 'docker',
args: standalone ? args : ['buildx', ...args]
};
}