Skip to content
Merged
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
147 changes: 146 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
windows-2022,
windows-2025,
windows-11-arm,
ubuntu-26.04,
ubuntu-26.04-arm,
]
steps:
- uses: actions/checkout@v6
Expand All @@ -38,6 +40,7 @@ jobs:
name: artifacts-test-${{ matrix.os }}
path: |
**/recordings/**/*

test-setup-ignore-tcc-db:
runs-on: ${{ matrix.os }}
strategy:
Expand All @@ -57,6 +60,32 @@ jobs:
path: |
**/recordings/**/*

test-setup-unsupported:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-24.04]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- run: yarn install --frozen-lockfile
- name: Setup should fail
run: |
set +e

yarn ci
exit_code=$?

if [ "$exit_code" -eq 0 ]; then
echo "Expected setup to fail, but it succeeded."

exit 1
fi

echo "Setup correctly failed with exit code $exit_code."

test-install:
runs-on: ${{ matrix.os }}
strategy:
Expand All @@ -71,7 +100,8 @@ jobs:
windows-2022,
windows-2025,
windows-11-arm,
ubuntu-latest,
ubuntu-26.04,
ubuntu-26.04-arm,
]
steps:
- uses: actions/checkout@v6
Expand All @@ -93,6 +123,110 @@ jobs:
{
"version": 1,
"screenReaders": [
{
"id": "orca",
"name": "Orca",
"platforms": ["linux"],
"defaultDownload": true,
"assets": [
{
"version": "0.2.0-50.2",
"platformVersion": "7",
"repository": "guidepup/orca",
"asset": "guidepup-orca-0.2.0-50.2.tar.gz",
"sha256": "9ce2b7962e6e3e5245782e1c2d628feeb2637484d32139dffe841fd585e07b71"
}
]
},
{
"id": "nvda",
"name": "NVDA",
"platforms": ["win32"],
"defaultDownload": true,
"assets": [
{
"version": "0.2.1-2026.1.1",
"repository": "guidepup/nvda",
"asset": "guidepup-nvda-0.2.1-2026.1.1.zip",
"sha256": "b6fdfde86190c7c14132d459f2f88995da6251ded431e23947d676ffb152f963"
}
]
},
{
"id": "voiceover",
"name": "VoiceOver",
"platforms": ["darwin"],
"defaultDownload": true,
"assets": [
{
"version": "0.0.1",
"platformVersion": "23",
"repository": "guidepup/voiceover",
"asset": "guidepup-voiceover-preferences-macos-14.dmg",
"sha256": "c6595f5ca50440e8553cde278936a46eff58d4c904e19c87e7d0e25950617ee1"
},
{
"version": "0.0.1",
"platformVersion": "24",
"repository": "guidepup/voiceover",
"asset": "guidepup-voiceover-preferences-macos-15.dmg",
"sha256": "8bdc3a11c45a19cc876a859bfbd64529469a8b7d4ad41fd7e00a0729ebdbcb25"
},
{
"version": "0.0.1",
"platformVersion": "25",
"repository": "guidepup/voiceover",
"asset": "guidepup-voiceover-preferences-macos-26.dmg",
"sha256": "9af2a3af7c9bffae1b2af26f1970548ecd62f33965fd30f4e43f21b9f57ce5ca"
}
]
}
]
}
EOF

guidepup install

test-install-unsupported:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- run: yarn install --frozen-lockfile
- run: yarn build
- shell: bash
run: |
tmpdir=$(mktemp -d)
cd "$tmpdir"
npm init -y
npm install @guidepup/guidepup
npm install -g "${GITHUB_WORKSPACE}"
export PATH="$(npm bin -g):$PATH"

cat > node_modules/@guidepup/guidepup/manifest.json <<'EOF'
{
"version": 1,
"screenReaders": [
{
"id": "orca",
"name": "Orca",
"platforms": ["linux"],
"defaultDownload": true,
"assets": [
{
"version": "0.2.0-50.2",
"platformVersion": "7",
"repository": "guidepup/orca",
"asset": "guidepup-orca-0.2.0-50.2.tar.gz",
"sha256": "9ce2b7962e6e3e5245782e1c2d628feeb2637484d32139dffe841fd585e07b71"
}
]
},
{
"id": "nvda",
"name": "NVDA",
Expand Down Expand Up @@ -140,4 +274,15 @@ jobs:
}
EOF

set +e

guidepup install
exit_code=$?

if [ "$exit_code" -eq 0 ]; then
echo "Expected install to fail, but it succeeded."

exit 1
fi

echo "Install correctly failed with exit code $exit_code."
21 changes: 21 additions & 0 deletions src/commands/install/download-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { handleInfoWithPath } from "../../logging";
import { getPlatformVersion } from "./get-platform-version";
import { platformMajorVersion } from "../../platform-major-version";
import { extractTarGz } from "./extract-tar-gz";

function selectAsset(screenReader: ScreenReader): Asset {
const currentPlatform = platform();
Expand Down Expand Up @@ -101,6 +102,26 @@ async function downloadScreenReader(

await extractZip(asset, downloadDestination, unzipDestination);

handleInfoWithPath(
`${screenReader.name} (${versionMessage}) successfully extracted to`,
unzipDestination,
);
} else if (asset.asset.endsWith(".tar.gz")) {
const unzipDestination = join(
cachePath,
screenReader.id,
getPlatformVersion(asset),
asset.version,
"extracted",
);

handleInfoWithPath(
`Extracting ${screenReader.name} (${versionMessage}) from`,
downloadDestination,
);

await extractTarGz(asset, downloadDestination, unzipDestination);

handleInfoWithPath(
`${screenReader.name} (${versionMessage}) successfully extracted to`,
unzipDestination,
Expand Down
46 changes: 46 additions & 0 deletions src/commands/install/extract-tar-gz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { execFile } from "node:child_process";
import { mkdir, readdir } from "node:fs/promises";
import { promisify } from "node:util";
import {
ERR_INSTALL_EMPTY_EXTRACTED_ASSET,
ERR_INSTALL_FAILED_TO_EXTRACT_ASSET,
} from "../../errors";
import { handleInfoWithPath } from "../../logging";
import type { Asset } from "./types";
import { deleteAsset } from "./delete-asset";

const execFileAsync = promisify(execFile);

export async function extractTarGz(
asset: Asset,
source: string,
destination: string,
): Promise<void> {
let files: string[];

try {
await mkdir(destination, { recursive: true });

await execFileAsync("tar", ["-xzf", source, "-C", destination]);

files = await readdir(destination);
} catch (cause) {
handleInfoWithPath("Unable to extract from", source);

await deleteAsset(source);
await deleteAsset(destination);

throw new Error(`${ERR_INSTALL_FAILED_TO_EXTRACT_ASSET}: ${asset.asset}`, {
cause,
});
}

if (!files.length) {
handleInfoWithPath("Extracted asset is empty at", destination);

await deleteAsset(source);
await deleteAsset(destination);

throw new Error(`${ERR_INSTALL_EMPTY_EXTRACTED_ASSET}: ${asset.asset}`);
}
}
12 changes: 4 additions & 8 deletions src/commands/install/extract-zip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ import { handleInfoWithPath } from "../../logging";
import type { Asset } from "./types";
import { deleteAsset } from "./delete-asset";

async function deleteAssets(...assets: string[]) {
for (const asset of assets) {
await deleteAsset(asset);
}
}

export async function extractZip(
asset: Asset,
source: string,
Expand All @@ -31,7 +25,8 @@ export async function extractZip(
} catch (cause) {
handleInfoWithPath("Unable to extract from", source);

await deleteAssets(source, destination);
await deleteAsset(source);
await deleteAsset(destination);

throw new Error(`${ERR_INSTALL_FAILED_TO_EXTRACT_ASSET}: ${asset.asset}`, {
cause,
Expand All @@ -41,7 +36,8 @@ export async function extractZip(
if (!files.length) {
handleInfoWithPath("Extracted asset is empty at", destination);

await deleteAssets(source, destination);
await deleteAsset(source);
await deleteAsset(destination);

throw new Error(`${ERR_INSTALL_EMPTY_EXTRACTED_ASSET}: ${asset.asset}`);
}
Expand Down
6 changes: 6 additions & 0 deletions src/commands/setup/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { platform } from "node:os";
import { Command } from "commander";
import { setup as setupMacOS } from "./macOS/setup";
import { setup as setupLinux } from "./linux/setup";
import { ERR_SETUP_UNSUPPORTED_OS } from "../../errors";
import { handleSetupError, handleSetupComplete } from "../../logging";

Expand All @@ -23,6 +24,11 @@ async function setup(options: SetupCommandOptions): Promise<void> {

break;
}
case "linux": {
await setupLinux();

break;
}
default: {
throw new Error(ERR_SETUP_UNSUPPORTED_OS);
}
Expand Down
44 changes: 44 additions & 0 deletions src/commands/setup/linux/getLinuxDistribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { readFile } from "node:fs/promises";
import { ERR_SETUP_LINUX_GET_DISTRIBUTION } from "../../../errors";

export interface LinuxDistribution {
id: string;
versionId: string;
}

export async function getLinuxDistribution(): Promise<LinuxDistribution> {
try {
const osRelease = await readFile("/etc/os-release", "utf8");

const values = Object.fromEntries(
osRelease
.split("\n")
.filter((line) => line.includes("="))
.map((line) => {
const separatorIndex = line.indexOf("=");

return [
line.slice(0, separatorIndex),
line.slice(separatorIndex + 1).replace(/^"|"$/g, ""),
];
}),
);

if (!values.ID) {
throw new Error("Missing required `ID` field in /etc/os-release.");
}

if (!values.VERSION_ID) {
throw new Error(
"Missing required `VERSION_ID` fields in /etc/os-release.",
);
}

return {
id: values.ID,
versionId: values.VERSION_ID,
};
} catch (cause) {
throw new Error(ERR_SETUP_LINUX_GET_DISTRIBUTION, { cause });
}
}
Loading
Loading