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: 5 additions & 2 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ jobs:
- name: Virtual display and sound server
run: |
sudo apt-get update -q
sudo apt-get install -y --no-install-recommends xvfb xauth libgl1 libglx-mesa0 libgl1-mesa-dri pulseaudio pulseaudio-utils
sudo apt-get install -y --no-install-recommends xvfb xauth libgl1 libglx-mesa0 libgl1-mesa-dri pulseaudio pulseaudio-utils openbox
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-$RUNNER_TEMP/xdg}"
mkdir -p "$XDG_RUNTIME_DIR" && chmod 700 "$XDG_RUNTIME_DIR"
echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" >> "$GITHUB_ENV"
Expand Down Expand Up @@ -560,10 +560,13 @@ jobs:
exit "$status"
- name: Firefox suites
if: ${{ !cancelled() }}
# Under a window manager, as on a real desktop: from Firefox 160 on, a fullscreen request on X11 waits for the
# window manager to make the window fullscreen, so on a bare Xvfb it never completes (Firefox 159 completed it).
run: |
status=0
firefox --version
xvfb-run -a -s "-screen 0 1920x1080x24" node web/tests/run.mjs platform scripts repeat keyboard framerate audio gamepad \
xvfb-run -a -s "-screen 0 1920x1080x24" bash -c 'openbox & sleep 1; exec node web/tests/run.mjs "$@"' firefox-suites \
platform scripts repeat keyboard framerate audio gamepad \
--browsers firefox --games "$GAME" --headed --out web-tests | tee web-tests-firefox.log || status=$?
grep -E '^(==|FAIL|[0-9]+ passed)' web-tests-firefox.log | sed 's/^/- /' >> "$GITHUB_STEP_SUMMARY" || true
exit "$status"
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,13 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
# The external project lives outside the repository, so global.json does not choose its SDK: the newest installed
# one builds it, as for a developer who installs the latest .NET. Installing that major here, instead of taking
# whatever the runner image ships, keeps every runner on the same SDK while GitHub rolls out new images.
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
dotnet-version: 11.0.x
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
Expand All @@ -132,7 +136,9 @@ jobs:
if: matrix.os == 'Linux'
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq xvfb > /dev/null
dotnet workload install wasm-tools android > /dev/null
# Workloads belong to one SDK: install them outside the repository, for the SDK the external project uses.
# SDK 11 builds the net10.0 web host with wasm-tools-net10 (wasm-tools serves net11.0 only), as `microengine doctor web` says.
(cd "$RUNNER_TEMP" && dotnet --version && dotnet workload install wasm-tools-net10 android > /dev/null)
npm ci --prefix web/tests > /dev/null
- uses: actions/setup-java@v4
if: matrix.os == 'Linux'
Expand Down
5 changes: 3 additions & 2 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -507,8 +507,9 @@ The macOS and iOS runtimes build only when macOS runners are on (daily CI passes
### Browser tests

`web/tests/run.mjs` plays development packages (`web/build.sh --dev`, SDL 3 in WebAssembly) in real browsers. In daily
CI (the **Browser engine** jobs, Chrome and Firefox on a virtual X server with a virtual sound server, one job per game
the change reaches) and in validate (all four games), it runs these suites, and the job fails when any of their checks
CI (the **Browser engine** jobs, Chrome and Firefox on a virtual X server with a virtual sound server, Firefox also
under a window manager (Openbox), without which Firefox 160 and later never complete a fullscreen request; one job per
game the change reaches) and in validate (all four games), it runs these suites, and the job fails when any of their checks
fails:

| Suite | What it shows |
Expand Down
3 changes: 2 additions & 1 deletion docs/examples/embedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ dotnet workload install wasm-tools
microengine package web
```

The first command installs .NET's WebAssembly build tools, once per machine. `package web` writes
The first command installs .NET's WebAssembly build tools, once per machine (with a .NET 11 SDK, `wasm-tools-net10`:
the tools for the game's .NET 10; `microengine doctor web` names the one your SDK needs). `package web` writes
`dist/<Game>-<version>-web/` and a ZIP of the same files. In browsers the game runs on the SDL platform compiled to
WebAssembly (SDL for the browser comes inside the `MicroEngine.Sdl` package), so every game that references
`MicroEngine.Sdl` or `MicroEngine.Desktop` can be packaged, with no code change.
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Builds a file a player installs, into `dist/` next to the project (or `--out`),
| `android` | APK for arm64-v8a and x86_64, signed with your key | any system | `android` workload, Android SDK, Java 17+ |
| `ios` | `.ipa` for iPhone and iPad, signed for your devices or the App Store | macOS | Xcode, `ios` workload, Apple Developer account |
| `ios-simulator` | ZIP holding the `.app` for Xcode's Simulator; **not installable on a device** | macOS | Xcode, `ios` workload |
| `web` | a static site (folder and ZIP) for any web server | any system | `wasm-tools` workload |
| `web` | a static site (folder and ZIP) for any web server | any system | `wasm-tools` workload (`wasm-tools-net10` with .NET SDK 11) |

[Packaging](packaging.md) describes each package, its requirements and limits; [Signing](signing.md) the keys.

Expand Down
6 changes: 4 additions & 2 deletions docs/guide/getting-microengine.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ terminal:
dotnet --version
```

It should print `10.0.100` or later. Desktop games need nothing else: SDL 3 and raylib come inside the NuGet packages.
Android, iOS and browser packages need extra .NET workloads; `microengine doctor` (step 4) lists them when you need them.
It should print `10.0.100` or later. A newer SDK, such as .NET 11, builds .NET 10 games too, and their packages carry
the .NET 10 runtime. Desktop games need nothing else: SDL 3 and raylib come inside the NuGet packages. Android, iOS and
browser packages need extra .NET workloads, installed for the SDK that builds your games (each SDK has its own);
`microengine doctor` (step 4) names the ones that SDK needs.

## 2. Install the `microengine` tool

Expand Down
4 changes: 3 additions & 1 deletion docs/guide/packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ SDL). Upload the folder to any static host (GitHub Pages, Netlify, an S3 bucket,
pre-compressed (`.br`, `.gz`) for servers that serve them. The game runs on SDL 3 compiled to WebAssembly, in the
same page in desktop and mobile browsers, with touch controls.

It needs the `wasm-tools` workload (`dotnet workload install wasm-tools`). SDL for the browser comes inside the
It needs the `wasm-tools` workload (`dotnet workload install wasm-tools`); with a newer .NET SDK than the game's
.NET 10, the workload for .NET 10 instead (`dotnet workload install wasm-tools-net10` with .NET SDK 11), because
`wasm-tools` serves only the SDK's own .NET. `microengine doctor web` names the one your SDK needs. SDL for the browser comes inside the
`MicroEngine.Sdl` package, built with the same emscripten as that workload, so any game that references
`MicroEngine.Sdl` or `MicroEngine.Desktop` can be packaged for the web (`--sdl-wasm FILE` names another SDL library).
10 changes: 10 additions & 0 deletions docs/guide/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ An eighth package: `MicroEngine.Physics.Box2D`, optional, for dynamic bodies.
Android and iOS packages from the game's `Configure(HostBuilder)` entry point only, and stops with the method to add
when a game has none. The tutorial's Gem Garden declares the entry point from its first step.

**Fixed**

- `microengine package` works with a .NET SDK newer than the game's .NET 10, such as .NET SDK 11: Windows and Linux
packages stopped because the tool looked for the licence of the SDK's own runtime (11.0) instead of the .NET 10
runtime the build used. Every package now carries the licence and notices of the runtime pack the build resolved,
and `build-info.json` names that runtime's version.
- `microengine doctor` accepts any .NET SDK from 10 on, and `doctor web` names the workload that SDK needs for a .NET 10
game: `wasm-tools-net10` with .NET SDK 11. `microengine info` shows the game's target framework (`net10.0`) instead
of the SDK's runtime version.

## 0.5.0-alpha.2

UI primitives for any game's interface, and a seventh package: `MicroEngine.Text.Shaping`, optional, for Arabic,
Expand Down
6 changes: 5 additions & 1 deletion docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ Daily CI runs the SDK check (`sdk.yml`) whenever a package input changes, and **
captures before and after must differ: input, drawing and a sprite from Content), packages it for each target the
runner can build (Windows, Linux, macOS, Android, web), checks each package's files and metadata, runs the desktop
package, plays the web package in Chrome, and checks the failures a developer meets (invalid id, missing or invalid
icon, missing signing key, a target the system cannot build, a missing workload).
icon, missing signing key, a target the system cannot build, a missing workload). The repository's `global.json`
does not reach that folder, so the newest installed .NET SDK builds the game, as on a developer's machine: the
workflow installs .NET SDK 11 next to the repository's SDK 10, and the Linux runner installs the workloads for
SDK 11 from outside the repository (`wasm-tools-net10`, `android`), because workloads belong to one SDK. SDK 10 is
covered by the repository's own package checks.
3. **The documented samples** (`sdk/tests/docs.mjs`, in the .NET Linux job): each sample the site quotes is copied
outside the repository and built against the packages this commit packs (the SDK built from source), and, once the
documented version is public, also from nuget.org at that version. The nuget.org build is only reported while the
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial/12-packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Orientation landscape
Factory new GemGarden.MainGame()
Content ...\GemGarden\Content\
Licence (none)
Engine MicroEngine 0.5.0-alpha.2, .NET 10.0.14
Engine MicroEngine 0.5.0-alpha.2, net10.0
```

`Licence (none)`: a `LICENSE`, `LICENSE.txt` or `LICENSE.md` file next to the project is shipped in every package as
Expand Down
3 changes: 2 additions & 1 deletion docs/tutorial/13-other-targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ microengine package web
```

The first command installs .NET's WebAssembly build tools once per machine (on Windows, from an administrator
terminal if .NET was installed for all users). The package is a folder and a ZIP of the same files:
terminal if .NET was installed for all users). With a .NET 11 SDK, install `wasm-tools-net10` instead: the tools for
the game's .NET 10 (`microengine doctor web` says which one your SDK needs). The package is a folder and a ZIP of the same files:
`dist/GemGarden-1.0.0-web/`, a static site with an `index.html` titled "Gem Garden", the icon as favicon, the .NET
runtime compiled to WebAssembly, SDL 3, the game and its `Content`.

Expand Down
34 changes: 30 additions & 4 deletions sdk/tests/external.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,17 @@ const fromPublic = args.includes('--public');
const feed = fromPublic ? null : path.resolve(option('feed') ?? path.join(repo, 'dist', 'sdk-feed'));
const version = option('version') ?? fs.readFileSync(path.join(repo, 'Directory.Build.props'), 'utf8').match(/<MicroEngineVersion>([^<]+)</)[1];
const host = { win32: 'windows', linux: 'linux', darwin: 'macos' }[process.platform];
// The project lives outside the repository, so global.json does not apply: the newest .NET SDK installed builds it, as
// for any developer, and workloads count only when installed for that SDK. Asked from a folder outside the repository.
const outsideRepo = fs.realpathSync(os.tmpdir());
const dotnetOutside = list => spawnSync('dotnet', list, { cwd: outsideRepo, encoding: 'utf8' }).stdout ?? '';
const sdkVersion = dotnetOutside(['--version']).trim();
// The browser workload for the template's net10.0 with that SDK, as `microengine doctor web` names it: wasm-tools serves
// only the SDK's own framework, so SDK 11 needs wasm-tools-net10.
const webWorkload = Number(sdkVersion.split('.')[0]) > 10 ? 'wasm-tools-net10' : 'wasm-tools';
const hasWorkload = (list, name) => new RegExp(`^\\s*${name}\\s`, 'm').test(list);
// By default: this system's own desktop target, Linux (cross-built from anywhere) and the web when the workload is here.
const hasWasm = /^\s*wasm-tools\s/m.test(spawnSync('dotnet', ['workload', 'list'], { encoding: 'utf8' }).stdout ?? '');
const hasWasm = hasWorkload(dotnetOutside(['workload', 'list']), webWorkload);
const targets = (option('targets') ?? [host, 'linux', ...(hasWasm ? ['web'] : [])].join(',')).split(',').filter((t, i, all) => t && all.indexOf(t) === i);
const exe = process.platform === 'win32' ? '.exe' : '';

Expand All @@ -47,9 +56,9 @@ if (feed && !fs.existsSync(feed) || feed && !fs.readdirSync(feed).some(f => f.st
console.error(`error: ${feed} holds no MicroEngine.Cli ${version} package; run: dotnet pack MicroEngine.slnx -c Release -o ${feed}`);
process.exit(2);
}
const work = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'microengine-external-'));
const work = fs.mkdtempSync(path.join(outsideRepo, 'microengine-external-'));
if (work.startsWith(repo)) throw new Error('the external project must live outside the repository');
console.log(`external project in ${work}, SDK ${version} from ${feed ?? 'nuget.org only'}`);
console.log(`external project in ${work}, SDK ${version} from ${feed ?? 'nuget.org only'}, .NET SDK ${sdkVersion}`);
const env = { ...process.env, NUGET_PACKAGES: path.join(work, 'nuget'), DOTNET_CLI_HOME: path.join(work, 'home'), DOTNET_NOLOGO: '1', DOTNET_CLI_TELEMETRY_OPTOUT: '1' };
for (const secret of ['ANDROID_KEYSTORE', 'ANDROID_KEYSTORE_PASSWORD', 'ANDROID_KEY_ALIAS', 'ANDROID_KEY_PASSWORD', 'MICROENGINE_IOS_CODESIGN_KEY', 'MICROENGINE_IOS_PROVISIONING_PROFILE']) delete env[secret];
delete env.MICROENGINE_DOTNET;
Expand Down Expand Up @@ -178,7 +187,13 @@ try {
}
if (process.platform !== 'win32') expectFailure('Windows package on another system', null, ['package', 'windows'], /built on Windows/);
const workloads = run('dotnet', ['workload', 'list']).out;
if (!/^\s*wasm-tools\s/m.test(workloads)) expectFailure('web package without the wasm-tools workload', null, ['package', 'web'], /wasm-tools/);
// A newer SDK also accepts the older framework's workload as installed when another SDK's wasm-tools left the same
// runtime pack on disk (packs are shared), so the failure is certain only without any browser runtime pack.
const dotnetRoot = dotnetOutside(['--list-sdks']).match(/\[(.+)\]/)?.[1];
const wasmPacks = dotnetRoot && path.join(path.dirname(dotnetRoot), 'packs', 'Microsoft.NETCore.App.Runtime.Mono.browser-wasm');
if (!hasWorkload(workloads, webWorkload) && !(wasmPacks && fs.existsSync(wasmPacks))) expectFailure(`web package without the ${webWorkload} workload`, null, ['package', 'web'], new RegExp(webWorkload));
r = run(me, ['doctor', 'web'], { cwd: game });
check(`microengine doctor web names the ${webWorkload} workload for .NET SDK ${sdkVersion}`, new RegExp(`${webWorkload} workload: (installed|dotnet workload install ${webWorkload})`).test(r.out), tail(r.out));
// Only a case-sensitive file system can hold both names; the build refuses them so every system loads the same file.
if (process.platform === 'linux') {
const twin = path.join(game, 'Content', 'Hero.png');
Expand Down Expand Up @@ -274,6 +289,14 @@ async function checkGallery() {
}
}

// A package carries the runtime of the game's target framework (net10.0: a 10.0 runtime) with its licence and notices,
// whichever SDK built it: SDK 11 publishes a net10.0 game with the 10.0 runtime, not its own 11.0.
function checkRuntime(target, info, has) {
const major = fs.readFileSync(path.join(work, 'Sample', 'Sample.csproj'), 'utf8').match(/<TargetFramework>net(\d+)\.\d+</)?.[1];
check(`${target}: the .NET runtime of the game's framework (net${major}) with its licence and notices`, String(info.dotnet).startsWith(`${major}.`)
&& has('dotnet-LICENSE.txt') && has('dotnet-THIRD-PARTY-NOTICES.txt'), `build-info dotnet ${info.dotnet}, .NET SDK ${sdkVersion}`);
}

function checkPackage(target, file, dist) {
const id = 'Sample';
if (['windows', 'linux', 'macos'].includes(target)) {
Expand All @@ -285,6 +308,7 @@ function checkPackage(target, file, dist) {
const info = JSON.parse(fs.readFileSync(path.join(top, 'build-info.json'), 'utf8'));
check(`${target}: build-info names the game, its version and the SDK`, info.game === id && info.title === 'Sample Game' && info.gameVersion === '1.2.3' && info.engineVersion === version && info.applicationId === 'com.example.sample', JSON.stringify(info));
const has = rel => fs.existsSync(path.join(top, rel));
checkRuntime(target, info, name => has(`licenses/${name}`));
const code = target === 'macos' ? path.join(top, 'Sample Game.app', 'Contents', 'MacOS') : top;
check(`${target}: executable, content, notices and README`, fs.existsSync(path.join(code, `${id}${target === 'windows' ? '.exe' : ''}`)) && has('README.txt') && has('THIRD-PARTY-NOTICES.txt') && has('licenses/microengine-LICENSE.txt')
&& (target === 'macos' ? has('Sample Game.app/Contents/Resources/Content/hero.png') : has('Content/hero.png')));
Expand Down Expand Up @@ -315,6 +339,7 @@ function checkPackage(target, file, dist) {
const manifest = JSON.parse(fs.readFileSync(path.join(folder, 'manifest.webmanifest'), 'utf8'));
check('web: app manifest and start screen', manifest.name === 'Sample Game' && manifest.icons?.[0]?.src === 'icon.png' && /rel="manifest"/.test(index)
&& /<img id="splash" src="splash.png"/.test(index) && fs.existsSync(path.join(folder, 'splash.png')) && /--splash: #[0-9a-f]{6}/.test(index), JSON.stringify(manifest));
checkRuntime('web', JSON.parse(fs.readFileSync(path.join(folder, 'build-info.json'), 'utf8')), name => fs.existsSync(path.join(folder, 'licenses', name)));
webPlays.push(folder);
} else if (target === 'android') {
const sdk = process.env.ANDROID_HOME;
Expand All @@ -328,6 +353,7 @@ function checkPackage(target, file, dist) {
const listing = run(path.join(tools, latest, `aapt${exe}`), ['list', file]).out;
check('android: adaptive icon, round icon and launch screen resources', ['res/mipmap-anydpi-v26/appicon_round.xml', 'res/mipmap-xxxhdpi-v4/appicon_foreground.png', 'res/mipmap-mdpi-v4/appicon.png', 'res/drawable/splash.xml']
.every(r => listing.includes(r)), tail(listing));
checkRuntime('android', JSON.parse(fs.readFileSync(file.replace(/\.apk$/, '.build-info.json'), 'utf8')), name => listing.includes(`assets/licenses/${name}`));
if (args.includes('--device')) {
const adb = path.join(sdk, 'platform-tools', `adb${exe}`);
const installed = run(adb, ['install', '-r', file]);
Expand Down
Loading
Loading