| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| import assert from 'node:assert/strict'; | |
| import childProcess from 'node:child_process'; | |
| import { EventEmitter } from 'node:events'; | |
| import { syncBuiltinESMExports } from 'node:module'; | |
| import { dirname, join } from 'node:path'; | |
| import { lstat, mkdir, mkdtemp, readdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'; | |
| import { tmpdir } from 'node:os'; | |
| import test from 'node:test'; | |
| import { buildCli, rustTarget, stageDesktopApp, stageDesktopBundle } from './build-cli.mjs'; | |
| const manifest = | |
| '{"id":"org.example.maka","name":"Maka & Co","version":"1.2.3-beta.1","entry":"main.js"}'; | |
| // A PNG signature and IHDR are all the icon staging reads. | |
| function logo(size = 1024) { | |
| const png = Buffer.alloc(33); | |
| Buffer.from('\x89PNG\r\n\x1a\n', 'latin1').copy(png); | |
| png.writeUInt32BE(13, 8); | |
| png.write('IHDR', 12, 'latin1'); | |
| png.writeUInt32BE(size, 16); | |
| png.writeUInt32BE(size, 20); | |
| return png; | |
| } | |
| async function repository(root) { | |
| const app = join(root, 'products/maka/desktop-shell/app'); | |
| await mkdir(join(app, 'icons'), { recursive: true }); | |
| await writeFile(join(app, 'gpui-shell.json'), manifest); | |
| await writeFile(join(app, 'main.js'), 'authored application'); | |
| await writeFile(join(app, 'icons/logo.svg'), '<svg/>'); | |
| await mkdir(join(root, 'assets'), { recursive: true }); | |
| await writeFile(join(root, 'assets/logo.png'), logo()); | |
| } | |
| test('CLI builds use the requested target and artifact path', async (t) => { | |
| const root = await mkdtemp(join(tmpdir(), 'maka-build-cli-')); | |
| t.after(() => rm(root, { recursive: true, force: true })); | |
| const sourceRoot = join(root, 'extracted-source'); | |
| for (const directory of [root, sourceRoot]) await repository(directory); | |
| const previous = process.env.CARGO_TARGET_DIR; | |
| process.env.CARGO_TARGET_DIR = 'target/isolated release'; | |
| let invocation; | |
| let exitCode = 0; | |
| t.mock.method(childProcess, 'spawn', (command, args, options) => { | |
| invocation = { command, args, options }; | |
| const child = new EventEmitter(); | |
| queueMicrotask(() => child.emit('exit', exitCode, null)); | |
| return child; | |
| }); | |
| syncBuiltinESMExports(); | |
| t.after(() => { | |
| t.mock.restoreAll(); | |
| syncBuiltinESMExports(); | |
| if (previous === undefined) delete process.env.CARGO_TARGET_DIR; | |
| else process.env.CARGO_TARGET_DIR = previous; | |
| }); | |
| for (const target of [ | |
| undefined, | |
| 'x86_64-unknown-linux-gnu', | |
| 'aarch64-unknown-linux-gnu', | |
| 'aarch64-apple-darwin', | |
| 'x86_64-pc-windows-msvc', | |
| ]) { | |
| const triple = target ?? rustTarget(); | |
| const linux = triple.includes('-linux-'); | |
| const directory = join(root, process.env.CARGO_TARGET_DIR); | |
| const binaryDirectory = join(directory, ...(linux || target ? [triple] : []), 'release'); | |
| const macos = triple.includes('-apple-darwin'); | |
| const app = macos ? 'Maka.app/Contents/Resources/app' : 'app'; | |
| for (const directory of new Set([app, 'app'])) { | |
| await mkdir(join(binaryDirectory, directory), { recursive: true }); | |
| await writeFile(join(binaryDirectory, directory, 'obsolete.js'), 'removed module'); | |
| } | |
| await writeFile(join(binaryDirectory, 'maka-desktop-shell'), 'shell bytes'); | |
| const output = await buildCli({ release: true, target, repositoryRoot: root }); | |
| await assert.rejects(lstat(join(dirname(output), app, 'obsolete.js')), { code: 'ENOENT' }); | |
| assert.equal( | |
| await readFile(join(dirname(output), app, 'main.js'), 'utf8'), | |
| 'authored application', | |
| ); | |
| assert.equal(await readFile(join(dirname(output), app, 'icons/logo.svg'), 'utf8'), '<svg/>'); | |
| if (macos) { | |
| const contents = join(dirname(output), 'Maka.app/Contents'); | |
| assert.equal( | |
| await readFile(join(contents, 'MacOS/maka-desktop-shell'), 'utf8'), | |
| 'shell bytes', | |
| ); | |
| if (process.platform !== 'win32') | |
| assert.equal((await lstat(join(contents, 'MacOS/maka-desktop-shell'))).mode & 0o777, 0o755); | |
| const plist = await readFile(join(contents, 'Info.plist'), 'utf8'); | |
| for (const [key, value] of [ | |
| ['CFBundleDisplayName', 'Maka & Co'], | |
| ['CFBundleExecutable', 'maka-desktop-shell'], | |
| ['CFBundleIconFile', 'Maka'], | |
| ['CFBundleIdentifier', 'org.example.maka'], | |
| ['CFBundleName', 'Maka & Co'], | |
| ['CFBundleShortVersionString', '1.2.3-beta.1'], | |
| ['CFBundleVersion', '1.2.3'], | |
| ]) | |
| assert.ok(plist.includes(`<key>${key}</key>\n <string>${value}</string>`), key); | |
| if (process.platform === 'darwin') | |
| childProcess.execFileSync('plutil', ['-lint', join(contents, 'Info.plist')]); | |
| const icon = await readFile(join(contents, 'Resources/Maka.icns')); | |
| assert.equal(icon.toString('latin1', 0, 4), 'icns'); | |
| assert.equal(icon.readUInt32BE(4), icon.length); | |
| assert.equal(icon.toString('latin1', 8, 12), 'ic10'); | |
| assert.equal(icon.readUInt32BE(12), icon.length - 8); | |
| assert.deepEqual(icon.subarray(16), logo()); | |
| await assert.rejects(lstat(join(dirname(output), 'app')), { code: 'ENOENT' }); | |
| } | |
| assert.equal(invocation.command, 'cargo'); | |
| assert.equal(invocation.options.cwd, root); | |
| assert.deepEqual(invocation.args, [ | |
| linux ? 'zigbuild' : 'build', | |
| '--locked', | |
| '-p', | |
| 'maka-cli', | |
| '-p', | |
| 'maka-desktop-shell', | |
| '-p', | |
| 'maka-python-runtime', | |
| '--target-dir', | |
| directory, | |
| '--release', | |
| ...(linux ? ['--target', triple + '.2.28'] : target ? ['--target', target] : []), | |
| ]); | |
| assert.equal( | |
| output, | |
| join( | |
| directory, | |
| ...(linux || target ? [triple] : []), | |
| 'release', | |
| triple.includes('-windows-') ? 'maka.exe' : 'maka', | |
| ), | |
| ); | |
| } | |
| await mkdir(join(root, 'target/debug'), { recursive: true }); | |
| await writeFile(join(root, 'target/debug/maka-desktop-shell'), 'shell bytes'); | |
| assert.equal( | |
| await buildCli({ repositoryRoot: root }), | |
| join(root, 'target/debug', process.platform === 'win32' ? 'maka.exe' : 'maka'), | |
| ); | |
| assert.deepEqual(invocation.args, [ | |
| 'build', | |
| '--locked', | |
| '-p', | |
| 'maka-cli', | |
| '-p', | |
| 'maka-desktop-shell', | |
| '-p', | |
| 'maka-python-runtime', | |
| '--target-dir', | |
| join(root, 'target'), | |
| ]); | |
| const sourceEnv = { MAKA_JS_DEPS: sourceRoot }; | |
| await mkdir(join(sourceRoot, 'target/aarch64-apple-darwin/release'), { recursive: true }); | |
| await writeFile( | |
| join(sourceRoot, 'target/aarch64-apple-darwin/release/maka-desktop-shell'), | |
| 'shell bytes', | |
| ); | |
| assert.equal( | |
| await buildCli({ | |
| release: true, | |
| target: 'aarch64-apple-darwin', | |
| repositoryRoot: sourceRoot, | |
| env: sourceEnv, | |
| }), | |
| join(sourceRoot, 'target/aarch64-apple-darwin/release/maka'), | |
| ); | |
| assert.equal(invocation.options.cwd, sourceRoot); | |
| assert.equal(invocation.options.env, sourceEnv); | |
| exitCode = 1; | |
| await assert.rejects(buildCli({ release: true, repositoryRoot: root }), /Maka build failed: 1/u); | |
| }); | |
| test('desktop app staging rejects symlinks without replacing the accepted sidecar', async () => { | |
| const root = await mkdtemp(join(tmpdir(), 'maka-app-staging-')); | |
| try { | |
| const source = join(root, 'products/maka/desktop-shell/app'); | |
| const destination = join(root, 'bin/app'); | |
| await mkdir(source, { recursive: true }); | |
| await writeFile(join(source, 'gpui-shell.json'), '{"entry":"main.js"}'); | |
| await writeFile(join(source, 'main.js'), 'original app'); | |
| await stageDesktopApp(root, destination); | |
| await writeFile(join(root, 'outside.js'), 'not authored app content'); | |
| await symlink(root, join(source, 'linked'), process.platform === 'win32' ? 'junction' : 'dir'); | |
| await assert.rejects(stageDesktopApp(root, destination), /non-regular file/); | |
| assert.equal(await readFile(join(destination, 'main.js'), 'utf8'), 'original app'); | |
| await assert.rejects(lstat(join(destination, 'linked')), { code: 'ENOENT' }); | |
| } finally { | |
| await rm(root, { recursive: true, force: true }); | |
| } | |
| }); | |
| test('a failed bundle staging keeps the previous Maka.app', async () => { | |
| const root = await mkdtemp(join(tmpdir(), 'maka-bundle-staging-')); | |
| try { | |
| await repository(root); | |
| const shell = join(root, 'maka-desktop-shell'); | |
| await writeFile(shell, 'original shell'); | |
| const destination = join(root, 'bin/Maka.app'); | |
| await mkdir(dirname(destination)); | |
| await stageDesktopBundle(root, shell, destination); | |
| await writeFile(shell, 'replacement shell'); | |
| const app = join(root, 'products/maka/desktop-shell/app'); | |
| await symlink(root, join(app, 'linked'), process.platform === 'win32' ? 'junction' : 'dir'); | |
| await assert.rejects(stageDesktopBundle(root, shell, destination), /non-regular file/); | |
| await rm(join(app, 'linked')); | |
| await writeFile(join(root, 'assets/logo.png'), logo(512)); | |
| await assert.rejects(stageDesktopBundle(root, shell, destination), /1024x1024 PNG/); | |
| await writeFile(join(root, 'assets/logo.png'), logo()); | |
| await writeFile(join(app, 'gpui-shell.json'), '{"entry":"main.js"}'); | |
| await assert.rejects(stageDesktopBundle(root, shell, destination), /has no id/); | |
| assert.equal( | |
| await readFile(join(destination, 'Contents/MacOS/maka-desktop-shell'), 'utf8'), | |
| 'original shell', | |
| ); | |
| assert.deepEqual(await readdir(dirname(destination)), ['Maka.app']); | |
| } finally { | |
| await rm(root, { recursive: true, force: true }); | |
| } | |
| }); |