mirror of
https://github.com/github/codeql-action.git
synced 2025-12-26 17:20:10 +08:00
331 lines
9.7 KiB
TypeScript
331 lines
9.7 KiB
TypeScript
import * as fs from "fs";
|
|
import * as os from "os";
|
|
import * as path from "path";
|
|
|
|
import * as toolrunner from "@actions/exec/lib/toolrunner";
|
|
import { IHeaders } from "@actions/http-client/interfaces";
|
|
import * as io from "@actions/io";
|
|
import * as actionsToolcache from "@actions/tool-cache";
|
|
import * as safeWhich from "@chrisgavin/safe-which";
|
|
import * as semver from "semver";
|
|
import { v4 as uuidV4 } from "uuid";
|
|
|
|
import { Logger } from "./logging";
|
|
import { isActions } from "./util";
|
|
|
|
/*
|
|
* This file acts as an interface to the functionality of the actions toolcache.
|
|
* That library is not safe to use outside of actions as it makes assumptions about
|
|
* the state of the filesystem and available environment variables.
|
|
*
|
|
* On actions we can just delegate to the toolcache library, however outside of
|
|
* actions we provide our own implementation.
|
|
*/
|
|
|
|
/**
|
|
* Extract a compressed tar archive.
|
|
*
|
|
* See extractTar function from node_modules/@actions/tool-cache/lib/tool-cache.d.ts
|
|
*
|
|
* @param file path to the tar
|
|
* @param mode should run the actions or runner implementation
|
|
* @param tempDir path to the temporary directory
|
|
* @param logger logger to use
|
|
* @returns path to the destination directory
|
|
*/
|
|
export async function extractTar(
|
|
file: string,
|
|
tempDir: string,
|
|
logger: Logger
|
|
): Promise<string> {
|
|
if (isActions()) {
|
|
return await actionsToolcache.extractTar(file);
|
|
} else {
|
|
// Initial implementation copied from node_modules/@actions/tool-cache/lib/tool-cache.js
|
|
|
|
if (!file) {
|
|
throw new Error("parameter 'file' is required");
|
|
}
|
|
// Create dest
|
|
const dest = createExtractFolder(tempDir);
|
|
// Determine whether GNU tar
|
|
logger.debug("Checking tar --version");
|
|
let versionOutput = "";
|
|
await new toolrunner.ToolRunner(
|
|
await safeWhich.safeWhich("tar"),
|
|
["--version"],
|
|
{
|
|
ignoreReturnCode: true,
|
|
silent: true,
|
|
listeners: {
|
|
stdout: (data) => (versionOutput += data.toString()),
|
|
stderr: (data) => (versionOutput += data.toString()),
|
|
},
|
|
}
|
|
).exec();
|
|
logger.debug(versionOutput.trim());
|
|
const isGnuTar = versionOutput.toUpperCase().includes("GNU TAR");
|
|
// Initialize args
|
|
const args = ["xz"];
|
|
if (logger.isDebug()) {
|
|
args.push("-v");
|
|
}
|
|
let destArg = dest;
|
|
let fileArg = file;
|
|
if (process.platform === "win32" && isGnuTar) {
|
|
args.push("--force-local");
|
|
destArg = dest.replace(/\\/g, "/");
|
|
// Technically only the dest needs to have `/` but for aesthetic consistency
|
|
// convert slashes in the file arg too.
|
|
fileArg = file.replace(/\\/g, "/");
|
|
}
|
|
if (isGnuTar) {
|
|
// Suppress warnings when using GNU tar to extract archives created by BSD tar
|
|
args.push("--warning=no-unknown-keyword");
|
|
}
|
|
args.push("-C", destArg, "-f", fileArg);
|
|
await new toolrunner.ToolRunner(`tar`, args).exec();
|
|
return dest;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Caches a directory and installs it into the tool cacheDir.
|
|
*
|
|
* Also see cacheDir function from node_modules/@actions/tool-cache/lib/tool-cache.d.ts
|
|
*
|
|
* @param sourceDir the directory to cache into tools
|
|
* @param tool tool name
|
|
* @param version version of the tool. semver format
|
|
* @param mode should run the actions or runner implementation
|
|
* @param toolCacheDir path to the tool cache directory
|
|
* @param logger logger to use
|
|
*/
|
|
export async function cacheDir(
|
|
sourceDir: string,
|
|
tool: string,
|
|
version: string,
|
|
toolCacheDir: string,
|
|
logger: Logger
|
|
): Promise<string> {
|
|
if (isActions()) {
|
|
return await actionsToolcache.cacheDir(sourceDir, tool, version);
|
|
} else {
|
|
// Initial implementation copied from node_modules/@actions/tool-cache/lib/tool-cache.js
|
|
|
|
version = semver.clean(version) || version;
|
|
const arch = os.arch();
|
|
logger.debug(`Caching tool ${tool} ${version} ${arch}`);
|
|
logger.debug(`source dir: ${sourceDir}`);
|
|
if (!fs.statSync(sourceDir).isDirectory()) {
|
|
throw new Error("sourceDir is not a directory");
|
|
}
|
|
// Create the tool dir
|
|
const destPath = createToolPath(tool, version, arch, toolCacheDir, logger);
|
|
// copy each child item. do not move. move can fail on Windows
|
|
// due to anti-virus software having an open handle on a file.
|
|
for (const itemName of fs.readdirSync(sourceDir)) {
|
|
const s = path.join(sourceDir, itemName);
|
|
await io.cp(s, destPath, { recursive: true });
|
|
}
|
|
// write .complete
|
|
completeToolPath(tool, version, arch, toolCacheDir, logger);
|
|
return destPath;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds the path to a tool version in the local installed tool cache.
|
|
*
|
|
* Also see find function from node_modules/@actions/tool-cache/lib/tool-cache.d.ts
|
|
*
|
|
* @param toolName name of the tool
|
|
* @param versionSpec version of the tool
|
|
* @param mode should run the actions or runner implementation
|
|
* @param toolCacheDir path to the tool cache directory
|
|
* @param logger logger to use
|
|
*/
|
|
export function find(
|
|
toolName: string,
|
|
versionSpec: string,
|
|
toolCacheDir: string,
|
|
logger: Logger
|
|
): string {
|
|
if (isActions()) {
|
|
return actionsToolcache.find(toolName, versionSpec);
|
|
} else {
|
|
// Initial implementation copied from node_modules/@actions/tool-cache/lib/tool-cache.js
|
|
|
|
if (!toolName) {
|
|
throw new Error("toolName parameter is required");
|
|
}
|
|
if (!versionSpec) {
|
|
throw new Error("versionSpec parameter is required");
|
|
}
|
|
const arch = os.arch();
|
|
// attempt to resolve an explicit version
|
|
if (!isExplicitVersion(versionSpec, logger)) {
|
|
const localVersions = findAllVersions(toolName, toolCacheDir, logger);
|
|
const match = evaluateVersions(localVersions, versionSpec, logger);
|
|
versionSpec = match;
|
|
}
|
|
// check for the explicit version in the cache
|
|
let toolPath = "";
|
|
if (versionSpec) {
|
|
versionSpec = semver.clean(versionSpec) || "";
|
|
const cachePath = path.join(toolCacheDir, toolName, versionSpec, arch);
|
|
logger.debug(`checking cache: ${cachePath}`);
|
|
if (fs.existsSync(cachePath) && fs.existsSync(`${cachePath}.complete`)) {
|
|
logger.debug(`Found tool in cache ${toolName} ${versionSpec} ${arch}`);
|
|
toolPath = cachePath;
|
|
} else {
|
|
logger.debug("not found");
|
|
}
|
|
}
|
|
return toolPath;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds the paths to all versions of a tool that are installed in the local tool cache.
|
|
*
|
|
* Also see findAllVersions function from node_modules/@actions/tool-cache/lib/tool-cache.d.ts
|
|
*
|
|
* @param toolName name of the tool
|
|
* @param toolCacheDir path to the tool cache directory
|
|
* @param logger logger to use
|
|
*/
|
|
export function findAllVersions(
|
|
toolName: string,
|
|
toolCacheDir: string,
|
|
logger: Logger
|
|
): string[] {
|
|
if (isActions()) {
|
|
return actionsToolcache.findAllVersions(toolName);
|
|
} else {
|
|
// Initial implementation copied from node_modules/@actions/tool-cache/lib/tool-cache.js
|
|
|
|
const versions: string[] = [];
|
|
const arch = os.arch();
|
|
const toolPath = path.join(toolCacheDir, toolName);
|
|
if (fs.existsSync(toolPath)) {
|
|
const children = fs.readdirSync(toolPath);
|
|
for (const child of children) {
|
|
if (isExplicitVersion(child, logger)) {
|
|
const fullPath = path.join(toolPath, child, arch || "");
|
|
if (
|
|
fs.existsSync(fullPath) &&
|
|
fs.existsSync(`${fullPath}.complete`)
|
|
) {
|
|
versions.push(child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return versions;
|
|
}
|
|
}
|
|
|
|
export async function downloadTool(
|
|
url: string,
|
|
tempDir: string,
|
|
headers: IHeaders
|
|
): Promise<string> {
|
|
const dest = path.join(tempDir, uuidV4());
|
|
const finalHeaders = Object.assign(
|
|
{ "User-Agent": "CodeQL Action" },
|
|
headers
|
|
);
|
|
return await actionsToolcache.downloadTool(
|
|
url,
|
|
dest,
|
|
undefined,
|
|
finalHeaders
|
|
);
|
|
}
|
|
|
|
function createExtractFolder(tempDir: string): string {
|
|
// create a temp dir
|
|
const dest = path.join(tempDir, "toolcache-temp");
|
|
if (!fs.existsSync(dest)) {
|
|
fs.mkdirSync(dest);
|
|
}
|
|
return dest;
|
|
}
|
|
|
|
function createToolPath(
|
|
tool: string,
|
|
version: string,
|
|
arch: string,
|
|
toolCacheDir: string,
|
|
logger: Logger
|
|
): string {
|
|
const folderPath = path.join(
|
|
toolCacheDir,
|
|
tool,
|
|
semver.clean(version) || version,
|
|
arch || ""
|
|
);
|
|
logger.debug(`destination ${folderPath}`);
|
|
const markerPath = `${folderPath}.complete`;
|
|
fs.rmdirSync(folderPath, { recursive: true });
|
|
fs.rmdirSync(markerPath, { recursive: true });
|
|
fs.mkdirSync(folderPath, { recursive: true });
|
|
return folderPath;
|
|
}
|
|
|
|
function completeToolPath(
|
|
tool: string,
|
|
version: string,
|
|
arch: string,
|
|
toolCacheDir: string,
|
|
logger: Logger
|
|
) {
|
|
const folderPath = path.join(
|
|
toolCacheDir,
|
|
tool,
|
|
semver.clean(version) || version,
|
|
arch || ""
|
|
);
|
|
const markerPath = `${folderPath}.complete`;
|
|
fs.writeFileSync(markerPath, "");
|
|
logger.debug("finished caching tool");
|
|
}
|
|
|
|
function isExplicitVersion(versionSpec: string, logger: Logger) {
|
|
const c = semver.clean(versionSpec) || "";
|
|
logger.debug(`isExplicit: ${c}`);
|
|
const valid = semver.valid(c) != null;
|
|
logger.debug(`explicit? ${valid}`);
|
|
return valid;
|
|
}
|
|
|
|
function evaluateVersions(
|
|
versions: string[],
|
|
versionSpec: string,
|
|
logger: Logger
|
|
): string {
|
|
let version = "";
|
|
logger.debug(`evaluating ${versions.length} versions`);
|
|
versions = versions.sort((a, b) => {
|
|
if (semver.gt(a, b)) {
|
|
return 1;
|
|
}
|
|
return -1;
|
|
});
|
|
for (let i = versions.length - 1; i >= 0; i--) {
|
|
const potential = versions[i];
|
|
const satisfied = semver.satisfies(potential, versionSpec);
|
|
if (satisfied) {
|
|
version = potential;
|
|
break;
|
|
}
|
|
}
|
|
if (version) {
|
|
logger.debug(`matched: ${version}`);
|
|
} else {
|
|
logger.debug("match not found");
|
|
}
|
|
return version;
|
|
}
|