Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci-aocc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
- { compiler: aocc, version: "5.0" }
- { compiler: aocc, version: "4.2" }
- { compiler: aocc, version: "4.1" }
include:
- image: ubuntu-26.04
toolchain: { compiler: aocc, version: "5.2" }

steps:
- uses: actions/checkout@v7
Expand Down
54 changes: 54 additions & 0 deletions __tests__/installers/aocc/debian.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,60 @@ describe("installDebian (AOCC)", () => {
);
});

it.each(["ubuntu26", "26.04"])(
"extracts the .deb instead of dpkg -i when osVersion is %s",
async (osVersion) => {
await installDebian({ ...baseInputs, osVersion });

const calls = mockedExec.mock.calls;
expect(calls).not.toContainEqual([
"sudo",
["dpkg", "-i", expect.anything()],
]);
expect(calls).toContainEqual([
"dpkg-deb",
[
"-x",
expect.stringContaining("aocc-compiler-5.1.0_1_amd64.deb"),
expect.stringContaining("aocc-deb-extract"),
],
]);
expect(calls).toContainEqual([
"sudo",
[
"cp",
"-rT",
expect.stringContaining(
"aocc-deb-extract/opt/AMD/aocc-compiler-5.1.0",
),
"/opt/AMD/aocc-compiler-5.1.0",
],
]);
const aptInstall = calls.find(
([, args]) =>
args?.includes("apt-get") && args.includes("libquadmath0"),
);
expect(aptInstall?.[1]).not.toContain("libxml2");
},
);

it.each(["ubuntu24", "ubuntu22", "22.04", "6.8.0-1010-azure"])(
"keeps dpkg -i when osVersion is %s",
async (osVersion) => {
await installDebian({ ...baseInputs, osVersion });

expect(mockedExec).toHaveBeenCalledWith("sudo", [
"dpkg",
"-i",
expect.stringContaining("aocc-compiler-5.1.0_1_amd64.deb"),
]);
expect(mockedExec).not.toHaveBeenCalledWith(
"dpkg-deb",
expect.anything(),
);
},
);

it("retries apt-get install -f on failure and eventually succeeds", async () => {
let attempts = 0;
mockedExec.mockImplementation(async (cmd, args, options) => {
Expand Down
56 changes: 45 additions & 11 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -102011,8 +102011,13 @@ async function aocc_debian_installDebian(inputs) {
`echo "${metadata.sha256} ${debPath}" | sha256sum -c -`,
]);
info(`Installing AOCC ${version}...`);
await exec_exec("sudo", ["dpkg", "-i", debPath]);
await aptGetFixInstallWithRetry();
if (dependsOnRenamedLibxml2(inputs.osVersion)) {
await installDebWithoutDependencyCheck(debPath, metadata.installDir);
}
else {
await exec_exec("sudo", ["dpkg", "-i", debPath]);
await aocc_debian_aptGetInstallWithRetry(["install", "-f", "-y"], "install -f");
}
info(`Saving AOCC ${version} to cache...`);
await exec_exec("sudo", ["mkdir", "-p", tempInstallDir]);
await exec_exec("sudo", ["cp", "-rT", metadata.installDir, tempInstallDir]);
Expand Down Expand Up @@ -102060,11 +102065,42 @@ async function aocc_debian_installDebian(inputs) {
};
return result;
}
// dpkg -i commonly leaves AOCC's declared dependencies unconfigured; this
// fixup step fetches them over apt, so it is exposed to the same transient
// mirror/network failures as any other apt-get call and needs the same
// retry-with-backoff handling.
async function aptGetFixInstallWithRetry(maxAttempts = 3) {
// Ubuntu 26.04 renamed libxml2 to libxml2-16, so the .deb's `Depends: libxml2`
// can't be satisfied. AOCC doesn't link libxml2, so the check is skipped there.
function dependsOnRenamedLibxml2(osVersion) {
const match = /ubuntu(\d+)|\b(\d+)\.04\b/.exec(osVersion);
const major = Number(match?.[1] ?? match?.[2]);
return major >= 26;
}
// The .deb's Depends, minus libxml2 (see above) and libncurses5-dev, which
// nothing in the package links.
const DEB_RUNTIME_DEPENDENCIES = [
"libstdc++6",
"libzstd1",
"libquadmath0",
"zlib1g",
"gcc",
];
// The .deb ships only /opt/AMD and has no maintainer scripts, so unpacking it
// is equivalent to installing it. Extract to a temp dir first: dpkg-deb -x
// onto / would reset the modes of / and /opt.
async function installDebWithoutDependencyCheck(debPath, installDir) {
await aocc_debian_aptGetInstallWithRetry(["install", "-y", ...DEB_RUNTIME_DEPENDENCIES], "install -y");
const extractDir = external_path_namespaceObject.posix.join(external_os_.tmpdir(), "aocc-deb-extract");
await exec_exec("rm", ["-rf", extractDir]);
await exec_exec("dpkg-deb", ["-x", debPath, extractDir]);
await exec_exec("sudo", ["mkdir", "-p", installDir]);
await exec_exec("sudo", [
"cp",
"-rT",
external_path_namespaceObject.posix.join(extractDir, installDir),
installDir,
]);
await exec_exec("rm", ["-rf", extractDir]);
}
// Used for both the `install -f` fixup and the explicit dependency install:
// apt fetches over the network, so transient mirror failures get retried.
async function aocc_debian_aptGetInstallWithRetry(args, label, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await exec_exec("sudo", [
Expand All @@ -102073,17 +102109,15 @@ async function aptGetFixInstallWithRetry(maxAttempts = 3) {
"--kill-after=30s",
"15m",
"apt-get",
"install",
"-f",
"-y",
...args,
...aocc_debian_APT_TIMEOUT_OPTS,
]);
return;
}
catch (err) {
if (attempt === maxAttempts)
throw err;
info(`apt-get install -f failed (attempt ${attempt.toString()}/${maxAttempts.toString()}), retrying in ${(attempt * 10).toString()}s...`);
info(`apt-get ${label} failed (attempt ${attempt.toString()}/${maxAttempts.toString()}), retrying in ${(attempt * 10).toString()}s...`);
await new Promise((res) => setTimeout(res, attempt * 10_000));
}
}
Expand Down
69 changes: 58 additions & 11 deletions src/installers/aocc/debian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,12 @@ export async function installDebian(
]);

core.info(`Installing AOCC ${version}...`);
await exec.exec("sudo", ["dpkg", "-i", debPath]);
await aptGetFixInstallWithRetry();
if (dependsOnRenamedLibxml2(inputs.osVersion)) {
await installDebWithoutDependencyCheck(debPath, metadata.installDir);
} else {
await exec.exec("sudo", ["dpkg", "-i", debPath]);
await aptGetInstallWithRetry(["install", "-f", "-y"], "install -f");
}

core.info(`Saving AOCC ${version} to cache...`);
await exec.exec("sudo", ["mkdir", "-p", tempInstallDir]);
Expand Down Expand Up @@ -170,11 +174,56 @@ export async function installDebian(
return result;
}

// dpkg -i commonly leaves AOCC's declared dependencies unconfigured; this
// fixup step fetches them over apt, so it is exposed to the same transient
// mirror/network failures as any other apt-get call and needs the same
// retry-with-backoff handling.
async function aptGetFixInstallWithRetry(maxAttempts = 3): Promise<void> {
// Ubuntu 26.04 renamed libxml2 to libxml2-16, so the .deb's `Depends: libxml2`
// can't be satisfied. AOCC doesn't link libxml2, so the check is skipped there.
function dependsOnRenamedLibxml2(osVersion: string): boolean {
const match = /ubuntu(\d+)|\b(\d+)\.04\b/.exec(osVersion);
const major = Number(match?.[1] ?? match?.[2]);
return major >= 26;
}

// The .deb's Depends, minus libxml2 (see above) and libncurses5-dev, which
// nothing in the package links.
const DEB_RUNTIME_DEPENDENCIES = [
"libstdc++6",
"libzstd1",
"libquadmath0",
"zlib1g",
"gcc",
] as const;

// The .deb ships only /opt/AMD and has no maintainer scripts, so unpacking it
// is equivalent to installing it. Extract to a temp dir first: dpkg-deb -x
// onto / would reset the modes of / and /opt.
async function installDebWithoutDependencyCheck(
debPath: string,
installDir: string,
): Promise<void> {
await aptGetInstallWithRetry(
["install", "-y", ...DEB_RUNTIME_DEPENDENCIES],
"install -y",
);

const extractDir = path.posix.join(os.tmpdir(), "aocc-deb-extract");
await exec.exec("rm", ["-rf", extractDir]);
await exec.exec("dpkg-deb", ["-x", debPath, extractDir]);
await exec.exec("sudo", ["mkdir", "-p", installDir]);
await exec.exec("sudo", [
"cp",
"-rT",
path.posix.join(extractDir, installDir),
installDir,
]);
await exec.exec("rm", ["-rf", extractDir]);
}

// Used for both the `install -f` fixup and the explicit dependency install:
// apt fetches over the network, so transient mirror failures get retried.
async function aptGetInstallWithRetry(
args: readonly string[],
label: string,
maxAttempts = 3,
): Promise<void> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await exec.exec("sudo", [
Expand All @@ -183,16 +232,14 @@ async function aptGetFixInstallWithRetry(maxAttempts = 3): Promise<void> {
"--kill-after=30s",
"15m",
"apt-get",
"install",
"-f",
"-y",
...args,
...APT_TIMEOUT_OPTS,
]);
return;
} catch (err) {
if (attempt === maxAttempts) throw err;
core.info(
`apt-get install -f failed (attempt ${attempt.toString()}/${maxAttempts.toString()}), retrying in ${(attempt * 10).toString()}s...`,
`apt-get ${label} failed (attempt ${attempt.toString()}/${maxAttempts.toString()}), retrying in ${(attempt * 10).toString()}s...`,
);
await new Promise((res) => setTimeout(res, attempt * 10_000));
}
Expand Down
Loading