mirror of
https://github.com/tauri-apps/tauri-action.git
synced 2026-01-31 00:35:20 +01:00
* Load correct config when a custom config is supplied * fmt Co-authored-by: Fabian-Lars <fabianlars@fabianlars.de>
535 lines
15 KiB
TypeScript
535 lines
15 KiB
TypeScript
import { platform } from 'os';
|
|
import { readFileSync, existsSync, copyFileSync, writeFileSync } from 'fs';
|
|
import { execa } from 'execa';
|
|
import { parse as parseToml } from '@iarna/toml';
|
|
import { join, resolve, normalize, sep } from 'path';
|
|
import { sync as globSync } from 'glob-gitignore';
|
|
import ignore from 'ignore';
|
|
import JSON5 from 'json5';
|
|
|
|
export function getPackageJson(root: string): any {
|
|
const packageJsonPath = join(root, 'package.json');
|
|
if (existsSync(packageJsonPath)) {
|
|
const packageJsonString = readFileSync(packageJsonPath).toString();
|
|
const packageJson = JSON.parse(packageJsonString);
|
|
return packageJson;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getTauriDir(root: string): string | null {
|
|
const ignoreRules = ignore();
|
|
const gitignorePath = join(root, '.gitignore');
|
|
if (existsSync(gitignorePath)) {
|
|
ignoreRules.add(readFileSync(gitignorePath).toString());
|
|
} else {
|
|
ignoreRules.add('node_modules').add('target');
|
|
}
|
|
const paths = globSync('**/tauri.conf.json', {
|
|
cwd: root,
|
|
ignore: ignoreRules,
|
|
});
|
|
const tauriConfPath = paths[0];
|
|
return tauriConfPath ? resolve(root, tauriConfPath, '..') : null;
|
|
}
|
|
|
|
function getWorkspaceDir(dir: string): string | null {
|
|
const rootPath = dir;
|
|
while (dir.length && dir[dir.length - 1] !== sep) {
|
|
const manifestPath = join(dir, 'Cargo.toml');
|
|
if (existsSync(manifestPath)) {
|
|
const toml = parseToml(readFileSync(manifestPath).toString());
|
|
// @ts-expect-error
|
|
if (toml.workspace?.members) {
|
|
// @ts-expect-error
|
|
const members: string[] = toml.workspace.members;
|
|
if (members.some((m) => resolve(dir, m) === rootPath)) {
|
|
return dir;
|
|
}
|
|
}
|
|
}
|
|
|
|
dir = normalize(join(dir, '..'));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getTargetDir(crateDir: string): string {
|
|
const def = join(crateDir, 'target');
|
|
if ('CARGO_TARGET_DIR' in process.env) {
|
|
return process.env.CARGO_TARGET_DIR ?? def;
|
|
}
|
|
let dir = crateDir;
|
|
while (dir.length && dir[dir.length - 1] !== sep) {
|
|
let cargoConfigPath = join(dir, '.cargo/config');
|
|
if (!existsSync(cargoConfigPath)) {
|
|
cargoConfigPath = join(dir, '.cargo/config.toml');
|
|
}
|
|
if (existsSync(cargoConfigPath)) {
|
|
const cargoConfig = parseToml(readFileSync(cargoConfigPath).toString());
|
|
// @ts-ignore
|
|
if (cargoConfig.build?.['target-dir']) {
|
|
// @ts-ignore
|
|
return cargoConfig.build['target-dir'];
|
|
}
|
|
}
|
|
|
|
dir = normalize(join(dir, '..'));
|
|
}
|
|
return def;
|
|
}
|
|
|
|
function hasDependency(dependencyName: string, root: string): boolean {
|
|
const packageJson = getPackageJson(root);
|
|
return (
|
|
packageJson &&
|
|
(packageJson.dependencies?.[dependencyName] ||
|
|
packageJson.devDependencies?.[dependencyName])
|
|
);
|
|
}
|
|
|
|
function usesYarn(root: string): boolean {
|
|
return existsSync(join(root, 'yarn.lock'));
|
|
}
|
|
|
|
export function execCommand(
|
|
command: string,
|
|
args: string[],
|
|
{ cwd }: { cwd?: string } = {}
|
|
): Promise<void> {
|
|
console.log(`running ${command}`, args);
|
|
return execa(command, args, {
|
|
cwd,
|
|
stdio: 'inherit',
|
|
env: { FORCE_COLOR: '0' },
|
|
}).then();
|
|
}
|
|
|
|
interface CargoManifestBin {
|
|
name: string;
|
|
}
|
|
|
|
interface CargoManifest {
|
|
package: { version: string; name: string; 'default-run': string };
|
|
bin: CargoManifestBin[];
|
|
}
|
|
|
|
interface TauriConfig {
|
|
package?: {
|
|
productName?: string;
|
|
version?: string;
|
|
};
|
|
tauri?: {
|
|
bundle?: {
|
|
identifier: string;
|
|
windows?: {
|
|
wix?: {
|
|
language?: string | string[] | { [language: string]: unknown };
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|
|
|
|
interface Application {
|
|
tauriPath: string;
|
|
runner: Runner;
|
|
name: string;
|
|
version: string;
|
|
wixLanguage: string | string[] | { [language: string]: unknown };
|
|
}
|
|
|
|
export interface BuildOptions {
|
|
configPath: string | null;
|
|
distPath: string | null;
|
|
iconPath: string | null;
|
|
tauriScript: string | null;
|
|
args: string[] | null;
|
|
bundleIdentifier: string | null;
|
|
}
|
|
|
|
export interface Runner {
|
|
runnerCommand: string;
|
|
runnerArgs: string[];
|
|
}
|
|
|
|
interface Info {
|
|
tauriPath: string | null;
|
|
name: string;
|
|
version: string;
|
|
wixLanguage: string | string[] | { [language: string]: unknown };
|
|
}
|
|
|
|
export interface Artifact {
|
|
path: string;
|
|
arch: string;
|
|
}
|
|
|
|
function _getJson5Config(contents: string): TauriConfig | null {
|
|
try {
|
|
const config = JSON5.parse(contents) as TauriConfig;
|
|
return config;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getConfig(path: string): TauriConfig {
|
|
const contents = readFileSync(path).toString();
|
|
try {
|
|
const config = JSON.parse(contents) as TauriConfig;
|
|
return config;
|
|
} catch (e) {
|
|
let json5Conf = _getJson5Config(contents);
|
|
if (json5Conf === null) {
|
|
json5Conf = _getJson5Config(
|
|
readFileSync(join(path, '..', 'tauri.conf.json5')).toString()
|
|
);
|
|
}
|
|
if (json5Conf) {
|
|
return json5Conf;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export function getInfo(root: string, inConfigPath: string | null = null): Info {
|
|
const tauriDir = getTauriDir(root);
|
|
if (tauriDir !== null) {
|
|
const configPath = inConfigPath ?? join(tauriDir, 'tauri.conf.json');
|
|
let name;
|
|
let version;
|
|
let wixLanguage: string | string[] | { [language: string]: unknown } =
|
|
'en-US';
|
|
const config = getConfig(configPath);
|
|
if (config.package) {
|
|
name = config.package.productName;
|
|
version = config.package.version;
|
|
if (config.package.version?.endsWith('.json')) {
|
|
const packageJsonPath = join(tauriDir, config.package.version);
|
|
const contents = readFileSync(packageJsonPath).toString();
|
|
version = JSON.parse(contents).version;
|
|
}
|
|
}
|
|
if (!(name && version)) {
|
|
const manifestPath = join(tauriDir, 'Cargo.toml');
|
|
const cargoManifest = parseToml(
|
|
readFileSync(manifestPath).toString()
|
|
) as any as CargoManifest;
|
|
name = name || cargoManifest.package.name;
|
|
version = version || cargoManifest.package.version;
|
|
}
|
|
if (config.tauri?.bundle?.windows?.wix?.language) {
|
|
wixLanguage = config.tauri.bundle.windows.wix.language;
|
|
}
|
|
|
|
if (!(name && version)) {
|
|
console.error('Could not determine package name and version');
|
|
process.exit(1);
|
|
}
|
|
|
|
return {
|
|
tauriPath: tauriDir,
|
|
name,
|
|
version,
|
|
wixLanguage,
|
|
};
|
|
} else {
|
|
const packageJson = getPackageJson(root);
|
|
const appName = packageJson
|
|
? (packageJson.displayName || packageJson.name).replace(/ /g, '-')
|
|
: 'app';
|
|
const version = packageJson ? packageJson.version : '0.1.0';
|
|
return {
|
|
tauriPath: null,
|
|
name: appName,
|
|
version,
|
|
wixLanguage: 'en-US',
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function buildProject(
|
|
root: string,
|
|
debug: boolean,
|
|
{
|
|
configPath,
|
|
distPath,
|
|
iconPath,
|
|
tauriScript,
|
|
args,
|
|
bundleIdentifier,
|
|
}: BuildOptions
|
|
): Promise<Artifact[]> {
|
|
return new Promise<Runner>((resolve, reject) => {
|
|
if (tauriScript) {
|
|
const [runnerCommand, ...runnerArgs] = tauriScript.split(' ');
|
|
resolve({ runnerCommand, runnerArgs });
|
|
} else if (
|
|
hasDependency('@tauri-apps/cli', root) ||
|
|
hasDependency('vue-cli-plugin-tauri', root)
|
|
) {
|
|
resolve(
|
|
usesYarn(root)
|
|
? { runnerCommand: 'yarn', runnerArgs: ['tauri'] }
|
|
: { runnerCommand: 'npx', runnerArgs: ['tauri'] }
|
|
);
|
|
} else {
|
|
execCommand('npm', ['install', '-g', '@tauri-apps/cli'], {
|
|
cwd: undefined,
|
|
})
|
|
.then(() => {
|
|
resolve({ runnerCommand: 'tauri', runnerArgs: [] });
|
|
})
|
|
.catch(reject);
|
|
}
|
|
})
|
|
.then((runner: Runner) => {
|
|
const info = getInfo(root, configPath);
|
|
if (info.tauriPath) {
|
|
return {
|
|
tauriPath: info.tauriPath,
|
|
runner,
|
|
name: info.name,
|
|
version: info.version,
|
|
wixLanguage: info.wixLanguage,
|
|
};
|
|
} else {
|
|
const packageJson = getPackageJson(root);
|
|
return execCommand(
|
|
runner.runnerCommand,
|
|
[...runner.runnerArgs, 'init', '--ci', '--app-name', info.name],
|
|
{
|
|
cwd: root,
|
|
}
|
|
).then(() => {
|
|
const tauriPath = getTauriDir(root);
|
|
if (tauriPath === null) {
|
|
console.error('Failed to resolve Tauri path');
|
|
process.exit(1);
|
|
}
|
|
const configPath = join(tauriPath, 'tauri.conf.json');
|
|
const config = getConfig(configPath);
|
|
|
|
console.log(
|
|
`Replacing tauri.conf.json config - package.version=${info.version}`
|
|
);
|
|
const pkgConfig = {
|
|
...config.package,
|
|
version: info.version,
|
|
};
|
|
if (packageJson?.productName) {
|
|
console.log(
|
|
`Replacing tauri.conf.json config - package.productName=${packageJson.productName}`
|
|
);
|
|
pkgConfig.productName = packageJson.productName;
|
|
}
|
|
config.package = pkgConfig;
|
|
|
|
if (bundleIdentifier) {
|
|
console.log(
|
|
`Replacing tauri.conf.json config - tauri.bundle.identifier=${bundleIdentifier}`
|
|
);
|
|
config.tauri = {
|
|
...config.tauri,
|
|
bundle: {
|
|
...config.tauri?.bundle,
|
|
identifier: bundleIdentifier,
|
|
},
|
|
};
|
|
}
|
|
|
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
|
|
const app = {
|
|
tauriPath,
|
|
runner,
|
|
name: info.name,
|
|
version: info.version,
|
|
wixLanguage: info.wixLanguage,
|
|
};
|
|
if (iconPath) {
|
|
return execCommand(
|
|
runner.runnerCommand,
|
|
[...runner.runnerArgs, 'icon', join(root, iconPath)],
|
|
{
|
|
cwd: root,
|
|
}
|
|
).then(() => app);
|
|
}
|
|
|
|
return app;
|
|
});
|
|
}
|
|
})
|
|
.then((app: Application) => {
|
|
const tauriConfPath = join(app.tauriPath, 'tauri.conf.json');
|
|
if (configPath !== null) {
|
|
copyFileSync(configPath, tauriConfPath);
|
|
}
|
|
|
|
if (distPath) {
|
|
const tauriConf = JSON.parse(readFileSync(tauriConfPath).toString());
|
|
tauriConf.build.distDir = distPath;
|
|
writeFileSync(tauriConfPath, JSON.stringify(tauriConf));
|
|
}
|
|
|
|
const tauriArgs = debug ? ['--debug', ...(args ?? [])] : args ?? [];
|
|
let buildCommand;
|
|
let buildArgs: string[] = [];
|
|
|
|
if (hasDependency('vue-cli-plugin-tauri', root)) {
|
|
if (usesYarn(root)) {
|
|
buildCommand = 'yarn';
|
|
buildArgs = ['tauri:build'];
|
|
} else {
|
|
buildCommand = 'npm';
|
|
buildArgs = ['run', 'tauri:build'];
|
|
}
|
|
} else {
|
|
buildCommand = app.runner.runnerCommand;
|
|
buildArgs = [...app.runner.runnerArgs, 'build'];
|
|
}
|
|
|
|
return execCommand(buildCommand, [...buildArgs, ...tauriArgs], {
|
|
cwd: root,
|
|
})
|
|
.then(() => {
|
|
let fileAppName = app.name;
|
|
// on Linux, the app product name is converted to kebab-case
|
|
if (!['darwin', 'win32'].includes(platform())) {
|
|
fileAppName = fileAppName
|
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
.replace(/([A-Z])([A-Z])(?=[a-z])/g, '$1-$2')
|
|
.replace(/[ _.]/g, '-')
|
|
.toLowerCase();
|
|
}
|
|
|
|
const cratePath = getWorkspaceDir(app.tauriPath) ?? app.tauriPath;
|
|
|
|
const found = [...tauriArgs].findIndex(
|
|
(e) => e === '-t' || e === '--target'
|
|
);
|
|
const targetPath = found >= 0 ? [...tauriArgs][found + 1] : '';
|
|
|
|
const artifactsPath = join(
|
|
getTargetDir(cratePath),
|
|
targetPath,
|
|
debug ? 'debug' : 'release'
|
|
);
|
|
|
|
let arch =
|
|
targetPath.search('-') >= 0
|
|
? targetPath.split('-')[0]
|
|
: process.arch;
|
|
|
|
if (platform() === 'darwin') {
|
|
if (arch === 'x86_64') {
|
|
arch = 'x64';
|
|
}
|
|
|
|
return [
|
|
join(
|
|
artifactsPath,
|
|
`bundle/dmg/${fileAppName}_${app.version}_${arch}.dmg`
|
|
),
|
|
join(artifactsPath, `bundle/macos/${fileAppName}.app`),
|
|
join(artifactsPath, `bundle/macos/${fileAppName}.app.tar.gz`),
|
|
join(artifactsPath, `bundle/macos/${fileAppName}.app.tar.gz.sig`),
|
|
].map((path) => ({ path, arch }));
|
|
} else if (platform() === 'win32') {
|
|
arch = arch.startsWith('i') ? 'x86' : 'x64';
|
|
|
|
// If multiple Wix languages are specified, multiple installers (.msi) will be made
|
|
// The .zip and .sig are only generated for the first specified language
|
|
let langs;
|
|
if (typeof app.wixLanguage === 'string') {
|
|
langs = [app.wixLanguage];
|
|
} else if (Array.isArray(app.wixLanguage)) {
|
|
langs = app.wixLanguage;
|
|
} else {
|
|
langs = Object.keys(app.wixLanguage);
|
|
}
|
|
const artifacts: string[] = [];
|
|
langs.forEach((lang) => {
|
|
artifacts.push(
|
|
join(
|
|
artifactsPath,
|
|
`bundle/msi/${fileAppName}_${app.version}_${arch}_${lang}.msi`
|
|
)
|
|
);
|
|
artifacts.push(
|
|
join(
|
|
artifactsPath,
|
|
`bundle/msi/${fileAppName}_${app.version}_${arch}_${lang}.msi.zip`
|
|
)
|
|
);
|
|
artifacts.push(
|
|
join(
|
|
artifactsPath,
|
|
`bundle/msi/${fileAppName}_${app.version}_${arch}_${lang}.msi.zip.sig`
|
|
)
|
|
);
|
|
});
|
|
return artifacts.map((path) => ({ path, arch }));
|
|
} else {
|
|
const debianArch =
|
|
arch === 'x64' || arch === 'x86_64'
|
|
? 'amd64'
|
|
: arch === 'x32' || arch === 'i686'
|
|
? 'i386'
|
|
: arch === 'arm'
|
|
? 'armhf'
|
|
: arch === 'aarch64'
|
|
? 'arm64'
|
|
: arch;
|
|
const appImageArch =
|
|
arch === 'x64' || arch === 'x86_64'
|
|
? 'amd64'
|
|
: arch === 'x32' || arch === 'i686'
|
|
? 'i386'
|
|
: arch;
|
|
|
|
return [
|
|
{
|
|
path: join(
|
|
artifactsPath,
|
|
`bundle/deb/${fileAppName}_${app.version}_${debianArch}.deb`
|
|
),
|
|
arch: debianArch,
|
|
},
|
|
{
|
|
path: join(
|
|
artifactsPath,
|
|
`bundle/appimage/${fileAppName}_${app.version}_${appImageArch}.AppImage`
|
|
),
|
|
arch: appImageArch,
|
|
},
|
|
{
|
|
path: join(
|
|
artifactsPath,
|
|
`bundle/appimage/${fileAppName}_${app.version}_${appImageArch}.AppImage.tar.gz`
|
|
),
|
|
arch: appImageArch,
|
|
},
|
|
{
|
|
path: join(
|
|
artifactsPath,
|
|
`bundle/appimage/${fileAppName}_${app.version}_${appImageArch}.AppImage.tar.gz.sig`
|
|
),
|
|
arch: appImageArch,
|
|
},
|
|
];
|
|
}
|
|
})
|
|
.then((artifacts) => {
|
|
console.log(
|
|
`Expected artifacts paths:\n${artifacts
|
|
.map((a) => a.path)
|
|
.join('\n')}`
|
|
);
|
|
return artifacts.filter((p) => existsSync(p.path));
|
|
});
|
|
});
|
|
}
|