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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ GitHub Action for installing [NASA GMAT](https://gmat.gsfc.nasa.gov/) (General M

## Status

setup-gmat installs GMAT (R2022a, R2025a, or R2026a) on Linux and Windows runners, caches the install across runs, and exports `GMAT_ROOT` to the workflow environment. macOS runner support is tracked under a future milestone; see the [Roadmap](#roadmap) below.
setup-gmat installs GMAT (R2022a, R2025a, or R2026a) on Linux, Windows, and macOS runners, caches the install across runs, and exports `GMAT_ROOT` to the workflow environment.

## What it does

Expand All @@ -32,8 +32,9 @@ setup-gmat installs GMAT (R2022a, R2025a, or R2026a) on Linux and Windows runner
| ---------------- | ---------------------- |
| `ubuntu-latest` | R2022a, R2025a, R2026a |
| `windows-latest` | R2022a, R2025a, R2026a |
| `macos-latest` | R2022a, R2025a, R2026a |

The action pulls GMAT's generic Linux x86_64 build on Linux runners (other Linux runners with a recent glibc should work) and the Windows x86_64 build on Windows runners. `ubuntu-latest` is the only configuration currently exercised in CI; cross-platform self-CI across all supported runner OSes is tracked under a future milestone. macOS runner support is also tracked under a future milestone.
The action pulls GMAT's generic Linux x86_64 build on Linux runners (other Linux runners with a recent glibc should work), the Windows x86_64 build on Windows runners, and the signed x86_64 macOS DMG on macOS runners (Apple Silicon runners install the same x86_64 build and run it under Rosetta). `ubuntu-latest` is the only configuration currently exercised in CI; cross-platform self-CI across all supported runner OSes is tracked under a future milestone.

## Quick start

Expand Down Expand Up @@ -80,7 +81,7 @@ Full documentation lives at **<https://astro-tools.github.io/setup-gmat/>** —
| Release | Scope |
| ---------------- | ---------------------------------------------------------------------------- |
| v0.1 _(current)_ | Linux + R2026a, basic caching, smoke check. |
| v0.2 | Linux + Windows, R2022a/R2025a/R2026a; macOS and weekly self-CI cron next. |
| v0.2 | Linux + Windows + macOS, R2022a/R2025a/R2026a; weekly self-CI cron next. |
| v0.3 | Docker image on GHCR, semantic-release wired up, cosign image signing. |
| v1.0 | Public API stability; at least two external consumers shipped on the action. |

Expand Down
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ The key is **not** sensitive to the action's minor or patch version, the upstrea

## Which GMAT versions are supported?

The action installs **R2022a, R2025a, or R2026a** on Linux and Windows runners. Any other value for `version` raises a validation error at parse time, before any download.
The action installs **R2022a, R2025a, or R2026a** on Linux, Windows, and macOS runners. Any other value for `version` raises a validation error at parse time, before any download.

NASA never released R2023a or R2024a — there are no SourceForge artifacts for those versions, and they will not appear in any version matrix this action ships. macOS runner support is tracked under a future milestone.
NASA never released R2023a or R2024a — there are no SourceForge artifacts for those versions, and they will not appear in any version matrix this action ships.

## Why does `python -c "import gmatpy"` fail in a step after `setup-gmat`?

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This page walks through the minimum CI workflow that installs GMAT and runs your

## Prerequisites

- A Linux or Windows runner. `ubuntu-latest` is the only configuration currently exercised in setup-gmat's CI. Other Linux runners with a recent glibc should work but are not covered by CI; cross-platform self-CI for `windows-latest` (and `macos-latest` when macOS support lands) is tracked under a future milestone.
- A Linux, Windows, or macOS runner. `ubuntu-latest` is the only configuration currently exercised in setup-gmat's CI. Other Linux runners with a recent glibc should work but are not covered by CI; cross-platform self-CI for `windows-latest` and `macos-latest` is tracked under a future milestone.
- `python` on `PATH` _before_ `setup-gmat` runs. The action shells out to `BuildApiStartupFile.py` and runs an internal smoke check; both invoke whatever Python `which python` resolves. The action does **not** bundle its own interpreter. Use [`actions/setup-python`](https://github.com/actions/setup-python) (or any equivalent) to put one on PATH.

## Minimum workflow
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A GitHub Action that installs [NASA GMAT](https://gmat.gsfc.nasa.gov/) and boots

## Status

setup-gmat installs GMAT (R2022a, R2025a, or R2026a) on Linux runners (`ubuntu-latest`) and Windows runners (`windows-latest`), caches across runs, and runs a built-in smoke check that loads and runs a stock GMAT sample. macOS runner support is tracked under a future milestone; see the [README roadmap](https://github.com/astro-tools/setup-gmat#roadmap) for the full plan.
setup-gmat installs GMAT (R2022a, R2025a, or R2026a) on Linux runners (`ubuntu-latest`), Windows runners (`windows-latest`), and macOS runners (`macos-latest`), caches across runs, and runs a built-in smoke check that loads and runs a stock GMAT sample.

## Where to next

Expand Down
15 changes: 15 additions & 0 deletions src/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ const WINDOWS_MIN_SIZE_BYTES: Record<GmatVersion, number> = {
R2026a: 380 * MIB,
};

// R2026a's signed DMG is ~455 MiB; 400 MiB matches the charter floor and
// gmat-run's working CI threshold. R2022a/R2025a floors are conservative
// truncation guards — the actual archives are larger.
const MACOS_MIN_SIZE_BYTES: Record<GmatVersion, number> = {
R2022a: 350 * MIB,
R2025a: 380 * MIB,
R2026a: 400 * MIB,
};

export async function download(version: GmatVersion): Promise<string> {
const spec = installerSpec(detectRunnerOs(), version);
core.info(`Downloading GMAT ${version} ${spec.archiveLabel} from ${spec.url}`);
Expand All @@ -48,6 +57,12 @@ function installerSpec(runnerOs: RunnerOs, version: GmatVersion): InstallerSpec
minBytes: WINDOWS_MIN_SIZE_BYTES[version],
archiveLabel: 'Windows installer',
};
case 'macos':
return {
url: `https://sourceforge.net/projects/gmat/files/GMAT/GMAT-${version}/gmat-mac-x64-${version}-signed.dmg/download`,
minBytes: MACOS_MIN_SIZE_BYTES[version],
archiveLabel: 'macOS installer',
};
}
}

Expand Down
90 changes: 87 additions & 3 deletions src/extract.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { randomUUID } from 'node:crypto';
import { existsSync } from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import * as io from '@actions/io';
import * as tc from '@actions/tool-cache';
import type { GmatVersion } from './inputs';
Expand Down Expand Up @@ -35,13 +37,95 @@ function unpack(archivePath: string, runnerOs: RunnerOs): Promise<string> {
return tc.extractTar(archivePath);
case 'windows':
return tc.extractZip(archivePath);
case 'macos':
return unpackDmg(archivePath);
}
}

async function unpackDmg(dmgPath: string): Promise<string> {
const id = randomUUID();
const mountPoint = path.join(runnerTemp(), `gmat-dmg-mount-${id}`);
const stagingDir = path.join(runnerTemp(), `gmat-dmg-stage-${id}`);
await io.mkdirP(stagingDir);

let attached = false;
try {
// -nobrowse hides the mount from Finder; -readonly is the default but
// explicit; -noautoopen prevents auto-launch of any app on the volume.
// Empty stdin defends against an interactive license prompt blocking the
// runner — `hdiutil` will fall back to its non-interactive path.
await exec.exec(
'hdiutil',
['attach', '-nobrowse', '-readonly', '-noautoopen', '-mountpoint', mountPoint, dmgPath],
{ input: Buffer.alloc(0) },
);
attached = true;

const apiScriptInMount = await findApiStartupScript(mountPoint);
const resolvedRoot = path.dirname(path.dirname(apiScriptInMount));
core.debug(`Resolved DMG GMAT root: ${resolvedRoot}`);

// cp -R because the mount is read-only, so io.mv would fail. -R on macOS
// BSD cp preserves symlinks as symlinks; Mach-O code signatures live
// inside the binaries themselves and bundle CodeResources are plain files
// in the tree, so the copy preserves signing without extra flags.
const stagedRoot = path.join(stagingDir, 'gmat');
await exec.exec('cp', ['-R', resolvedRoot, stagedRoot]);
} finally {
if (attached) {
try {
await exec.exec('hdiutil', ['detach', mountPoint]);
} catch (err) {
core.warning(
`hdiutil detach failed for ${mountPoint}: ${err instanceof Error ? err.message : String(err)}. ` +
`Self-hosted runners may need manual cleanup.`,
);
}
}
await io.rmRF(mountPoint).catch(() => undefined);
}

return stagingDir;
}

async function findApiStartupScript(mountPoint: string): Promise<string> {
// The DMG's wrapper folder name is not deterministic across versions, so
// resolve the GMAT root by locating BuildApiStartupFile.py and walking up
// two levels (the script lives at <gmat_root>/api/BuildApiStartupFile.py).
let stdout = '';
await exec.exec('find', [mountPoint, '-name', 'BuildApiStartupFile.py', '-print', '-quit'], {
listeners: {
stdout: (data: Buffer) => {
stdout += data.toString();
},
},
});
const match = stdout.split('\n').find((line) => line.trim().length > 0);
if (match === undefined) {
throw new Error(
`Could not locate BuildApiStartupFile.py inside the mounted DMG at ${mountPoint}. ` +
`Did the upstream archive layout change?`,
);
}
return match.trim();
}

function locateGmatRoot(stagingDir: string, version: GmatVersion, runnerOs: RunnerOs): string {
// The Linux tarball wraps its contents in `GMAT/<version>/`; the Windows zip
// has no wrapper and unpacks the install layout directly at the staging root.
const expectedRoot = runnerOs === 'linux' ? path.join(stagingDir, 'GMAT', version) : stagingDir;
// Linux: the tarball wraps in `GMAT/<version>/`. macOS: unpackDmg already
// resolves the GMAT root inside the mount and copies it to `<staging>/gmat`.
// Windows: the zip has no wrapper; install layout sits at the staging root.
let expectedRoot: string;
switch (runnerOs) {
case 'linux':
expectedRoot = path.join(stagingDir, 'GMAT', version);
break;
case 'macos':
expectedRoot = path.join(stagingDir, 'gmat');
break;
case 'windows':
expectedRoot = stagingDir;
break;
}
if (existsSync(path.join(expectedRoot, API_STARTUP_FILE))) {
return expectedRoot;
}
Expand Down
13 changes: 4 additions & 9 deletions src/os.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
export type RunnerOs = 'linux' | 'windows';
export type RunnerOs = 'linux' | 'windows' | 'macos';

export const SUPPORTED_RUNNER_OS: readonly RunnerOs[] = ['linux', 'windows'];
export const SUPPORTED_RUNNER_OS: readonly RunnerOs[] = ['linux', 'windows', 'macos'];

export function detectRunnerOs(): RunnerOs {
const raw = process.env.RUNNER_OS;
if (raw === 'Linux') return 'linux';
if (raw === 'Windows') return 'windows';
if (raw === 'macOS') {
throw new Error(
'macOS runners are not yet supported by setup-gmat. ' +
'See the project roadmap for cross-platform support.',
);
}
if (raw === 'macOS') return 'macos';
throw new Error(
`Unsupported runner OS "${raw ?? '<unset>'}". ` +
`setup-gmat supports: ${SUPPORTED_RUNNER_OS.join(', ')}. ` +
`Set RUNNER_OS to Linux or Windows (GitHub-hosted runners do this automatically).`,
`Set RUNNER_OS to Linux, Windows, or macOS (GitHub-hosted runners do this automatically).`,
);
}
Loading