dootask/electron/build.js
kuaifan bad7634f27 ci(app): App 改由 Expo 壳构建发布,移除旧 eeui 子模块 resources/mobile
- 新增可复用工作流 app-build.yml(web 资源 / Android 正式签名 APK / iOS IPA)
- publish.yml 的 Android 改用 Expo 构建并上传 R2 与 Release
- ios-publish.yml 改为构建 Expo App 并上传 App Store Connect(构建号自动递增)
- app-internal.yml 改为调用 app-build.yml 出内测包
- 移除 ./cmd appbuild/eeui 与 electron/build.js 的 eeui 打包流程
2026-10-20 10:29:27 +00:00

813 lines
31 KiB
JavaScript
Vendored
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require('fs');
const fse = require('fs-extra');
const path = require('path')
const inquirer = require('inquirer');
const child_process = require('child_process');
const ora = require('ora');
const yauzl = require('yauzl');
const axios = require('axios');
const tar = require('tar');
const utils = require('./lib/utils');
const r2 = require('./lib/r2');
const { buildReleaseIndex } = require('./lib/release-index');
const config = require('../package.json')
const env = require('dotenv').config({ path: './.env' })
const argv = process.argv;
const {BUILD_FRONTEND, APPLEID, APPLEIDPASS, GITHUB_TOKEN, GITHUB_REPOSITORY} = process.env;
const electronDir = path.resolve(__dirname, "public");
const nativeCachePath = path.resolve(__dirname, ".native");
const devloadCachePath = path.resolve(__dirname, ".devload");
const packageFile = path.resolve(__dirname, "package.json");
const packageBakFile = path.resolve(__dirname, "package-bak.json");
const platforms = ["build-mac", "build-win"];
const architectures = ["arm64", "x64"];
let buildChecked = false,
updaterChecked = false;
const elapsedSeconds = (startTime) => `${((Date.now() - startTime) / 1000).toFixed(1)}s`;
/**
* 检测并下载更新器
*/
async function detectAndDownloadUpdater() {
const updaterDir = path.resolve(__dirname, "updater");
const latestVersionFile = path.join(updaterDir, "latest");
// 创建updater目录
if (!fs.existsSync(updaterDir)) {
fs.mkdirSync(updaterDir, { recursive: true });
}
try {
// 获取最新release
const spinner = ora('检查更新器...').start();
const response = await axios.get('https://api.github.com/repos/kuaifan/dootask-updater/releases/latest', {
headers: GITHUB_TOKEN ? { 'Authorization': `token ${GITHUB_TOKEN}` } : {}
});
if (!response.data || !response.data.assets) {
spinner.fail('检查更新器失败');
return;
}
// 检查版本是否需要更新
const latestVersion = response.data.tag_name || response.data.name;
let currentVersion = '';
if (fs.existsSync(latestVersionFile)) {
currentVersion = fs.readFileSync(latestVersionFile, 'utf8').trim();
}
// 如果版本不一致,清空updater目录(保留latest文件)
if (currentVersion !== latestVersion) {
const files = fs.readdirSync(updaterDir);
for (const file of files) {
if (file === 'latest') continue;
const filePath = path.join(updaterDir, file);
if (fs.lstatSync(filePath).isDirectory()) {
fs.rmSync(filePath, { recursive: true });
} else {
fs.unlinkSync(filePath);
}
}
}
// 过滤出binary开头的zip文件
const assets = response.data.assets.filter(asset =>
asset.name.startsWith('binary_') && asset.name.endsWith('.zip')
);
spinner.succeed('检查更新器成功');
// 下载并解压每个文件
for (const asset of assets) {
const fileName = asset.name;
// 解析平台和架构信息 (binary_0.1.0_linux-x86_64.zip => linux/x86_64)
const match = fileName.match(/binary_[\d.]+_(.+)-(.+)\.zip$/);
if (!match) continue;
let [, platform, arch] = match;
// 平台名称映射
const platformMap = {
'macos': 'mac',
'windows': 'win'
};
platform = platformMap[platform] || platform;
// 架构名称映射
const archMap = {
'x86_64': 'x64'
};
arch = archMap[arch] || arch;
const targetDir = path.join(updaterDir, platform, arch);
const zipPath = path.join(updaterDir, fileName);
// 检查是否已经下载过
if (fs.existsSync(targetDir)) {
continue;
}
// 创建目标目录
fs.mkdirSync(targetDir, { recursive: true });
// 下载文件
const downloadSpinner = ora(`下载 ${fileName}...`).start();
try {
const writer = fs.createWriteStream(zipPath);
const response = await axios({
url: asset.browser_download_url,
method: 'GET',
responseType: 'stream',
headers: GITHUB_TOKEN ? { 'Authorization': `token ${GITHUB_TOKEN}` } : {}
});
response.data.pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
// 解压文件
downloadSpinner.text = `解压 ${fileName}...`;
await new Promise((resolve, reject) => {
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
if (err) reject(err);
zipfile.readEntry();
zipfile.on('entry', (entry) => {
if (/\/$/.test(entry.fileName)) {
zipfile.readEntry();
} else {
zipfile.openReadStream(entry, (err, readStream) => {
if (err) reject(err);
const outputPath = path.join(targetDir, path.basename(entry.fileName));
const writer = fs.createWriteStream(outputPath);
readStream.pipe(writer);
writer.on('finish', () => {
fs.chmodSync(outputPath, 0o755);
zipfile.readEntry();
});
});
}
});
zipfile.on('end', resolve);
});
});
// 删除zip文件
fs.unlinkSync(zipPath);
downloadSpinner.succeed(`下载并解压 ${fileName} 成功`);
// 下载和解压成功后,保存最新版本号
fs.writeFileSync(latestVersionFile, latestVersion, 'utf8');
} catch (error) {
downloadSpinner.fail(`下载 ${fileName} 失败: ${error.message}`);
// 清理失败的下载
if (fs.existsSync(zipPath)) {
fs.unlinkSync(zipPath);
}
if (fs.existsSync(targetDir)) {
fs.rmdSync(targetDir, { recursive: true });
}
}
}
} catch (error) {
console.error('检查更新器失败:', error.message);
}
}
/**
* 下载并解压Drawio
* @param drawioDestDir
*/
async function downloadAndExtractDrawio(drawioDestDir) {
const tempDir = path.resolve(__dirname, ".temp");
const tarFilePath = path.join(tempDir, "drawio-latest.tar.gz");
try {
// 创建临时目录
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
const spinner = ora('下载最新的Drawio文件...').start();
// 1. 下载tar.gz文件
const response = await axios({
url: 'https://appstore.dootask.com/api/v1/download/drawio/latest',
method: 'GET',
responseType: 'stream'
});
const writer = fs.createWriteStream(tarFilePath);
response.data.pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
spinner.text = '解压Drawio文件...';
// 2. 解压tar.gz文件到临时目录
const extractDir = path.join(tempDir, "extracted");
if (fs.existsSync(extractDir)) {
fse.removeSync(extractDir);
}
fs.mkdirSync(extractDir, { recursive: true });
await tar.x({
file: tarFilePath,
cwd: extractDir
});
// 3. 查找符合版本号格式的文件夹
const files = fs.readdirSync(extractDir);
const versionRegex = /^v?\d+(\.\d+){1,2}$/;
const versionDir = files.find(file => {
const filePath = path.join(extractDir, file);
return fs.lstatSync(filePath).isDirectory() && versionRegex.test(file);
});
if (!versionDir) {
throw new Error('未找到符合版本号格式的文件夹');
}
// 4. 查找webapp文件夹
const versionPath = path.join(extractDir, versionDir);
const webappPath = path.join(versionPath, 'webapp');
if (!fs.existsSync(webappPath)) {
throw new Error('未找到webapp文件夹');
}
// 5. 复制webapp文件夹内容到目标目录
fse.copySync(webappPath, drawioDestDir);
spinner.succeed('Drawio文件下载并解压完成');
// 清理临时文件
fse.removeSync(tempDir);
} catch (error) {
console.warn('下载Drawio失败,使用默认版本:', error.message);
// 清理临时文件
if (fs.existsSync(tempDir)) {
fse.removeSync(tempDir);
}
}
}
/**
* 克隆 Drawio
* @param systemInfo
*/
async function cloneDrawio(systemInfo) {
child_process.execSync("git submodule update --quiet --init --depth=1", {stdio: "inherit"});
const drawioSrcDir = path.resolve(__dirname, "../resources/drawio/src/main/webapp");
const drawioDestDir = path.resolve(electronDir, "drawio/webapp");
fse.copySync(drawioSrcDir, drawioDestDir)
await downloadAndExtractDrawio(drawioDestDir);
const preConfigFile = path.resolve(drawioDestDir, "js/PreConfig.js");
if (!fs.existsSync(preConfigFile)) {
console.error("克隆 Drawio 失败!");
process.exit()
}
let preConfigString = fs.readFileSync(preConfigFile, 'utf8');
preConfigString += "\nwindow.systemInfo = " + JSON.stringify(systemInfo) + ";\n";
preConfigString += fs.readFileSync(path.resolve(__dirname, "drawio.js"), 'utf8');
fs.writeFileSync(preConfigFile, preConfigString, 'utf8');
}
/**
* 获取更新日志
* @returns {string}
*/
function changeLog() {
let filePath = path.resolve(__dirname, "../CHANGELOG.md");
if (!fs.existsSync(filePath)) {
return "";
}
let content = fs.readFileSync(filePath, 'utf8')
let array = content.match(/## \[([0-9]+.+)\]/g)
if (!array) {
return ""
}
let start = content.indexOf(array[0]);
if (array.length > 5) {
content = content.substring(start, content.indexOf(array[5]))
} else {
content = content.substring(start)
}
return content;
}
/**
* 上传单个文件到 R2 的 draft/<version>/ 目录(带进度/spinner)
*/
async function uploadDraftFile(client, localFile, version) {
const filename = path.basename(localFile);
const key = `draft/${version}/${filename}`;
const startTime = Date.now();
const spinner = ora(`Upload [0%] ${filename}`).start();
try {
await r2.uploadFile(client, localFile, key, (loaded, total) => {
const pct = Math.min(99, Math.round((loaded / total) * 100)) + '%';
spinner.text = `Upload [${pct}] ${filename}`;
});
} catch (error) {
spinner.fail(`Upload [fail] ${filename} (${elapsedSeconds(startTime)}): ${error.message || error}`);
throw error;
}
spinner.succeed(`Upload [100%] ${filename} (${elapsedSeconds(startTime)})`);
}
/**
* 生成配置、编译应用
* @param data
*/
async function startBuild(data) {
if ((BUILD_FRONTEND === 'build' || data.id === 'app-expo') && !buildChecked) {
buildChecked = true
// Expo 每次重新编译,避免把上次桌面端或旧版本的文件带进内测包。
if (data.id === 'app-expo') {
fse.emptyDirSync(electronDir);
}
fs.mkdirSync(electronDir, { recursive: true });
fse.copySync(path.resolve(__dirname, "index.html"), path.resolve(electronDir, "index.html"))
child_process.execSync("npx vite build -- fromcmd electronBuild", {stdio: "inherit"});
}
//
const {platform, archs, publish, release, notarize} = data.configure
// system info
const systemInfo = {
title: data.name,
loginLogo: "",
debug: "no",
version: config.version,
origin: "./",
homeUrl: utils.formatUrl(data.url),
apiUrl: utils.formatUrl(data.url) + "api/",
}
// information
if (data.id === 'app-expo') {
console.log("\n=== 编译信息 ===");
console.log("名称:", data.name);
console.log("版本:", config.version + ` (${config.codeVerson})`);
console.log("===============\n");
} else {
console.log("\n=== 编译信息 ===");
console.log("名称:", data.name);
console.log("应用ID:", data.id);
console.log("版本:", config.version + ` (${config.codeVerson})`);
console.log("系统:", platform.replace('build-', '').toUpperCase());
console.log("架构:", archs.map(arch => arch.toUpperCase()).join(', '));
console.log("发布:", publish ? `是(${release ? '提示升级' : '静默升级'})` : '否');
if (platform === 'build-mac') {
console.log("公证:", notarize ? '是' : '否');
}
console.log("===============\n");
// drawio
await cloneDrawio(systemInfo)
// updater
if (!updaterChecked) {
updaterChecked = true
await detectAndDownloadUpdater()
}
}
// language
fse.copySync(path.resolve(__dirname, "../public/language"), path.resolve(electronDir, "language"))
// config.js
fs.writeFileSync(electronDir + "/config.js", "window.systemInfo = " + JSON.stringify(systemInfo), 'utf8');
fs.writeFileSync(electronDir + "/dark", '', 'utf8');
if (data.id !== 'app-expo') {
fs.writeFileSync(nativeCachePath, utils.formatUrl(data.url));
fs.writeFileSync(devloadCachePath, "", 'utf8');
}
// index.html
let manifestFile = path.resolve(electronDir, "manifest.json");
if (!fs.existsSync(manifestFile)) {
if (data.id === 'app-expo') {
throw new Error("manifest.json 未找到,Expo 前端构建失败");
}
console.error("manifest.json 未找到");
return;
}
let manifestContent = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
let indexFile = path.resolve(electronDir, "index.html");
let indexString = fs.readFileSync(indexFile, 'utf8');
indexString = indexString.replace(/<title>(.*?)<\/title>/g, `<title>${data.name}</title>`);
indexString = indexString.replace("<!--style-->", `<link rel="stylesheet" type="text/css" href="./${manifestContent['resources/assets/js/app.js']['css'][0]}">`);
indexString = indexString.replace("<!--script-->", `<script type="module" src="./${manifestContent['resources/assets/js/app.js']['file']}"></script>`);
fs.writeFileSync(indexFile, indexString, 'utf8');
//
if (data.id === 'app-expo') {
const appDir = path.resolve(__dirname, "../resources/app");
const webDir = path.join(appDir, "assets/web");
fse.emptyDirSync(webDir);
fse.copySync(electronDir, webDir);
fs.writeFileSync(path.join(appDir, "version.json"), JSON.stringify({
version: config.version,
codeVerson: config.codeVerson,
}, null, 2) + "\n", 'utf8');
console.log("Expo 前端资源已同步至 resources/app/assets/web/");
return;
}
const output = `dist/${data.id.replace(/\./g, '-')}/${platform}`
// package.json Backup
fse.copySync(packageFile, packageBakFile)
const recoveryPackage = (onlyRecovery) => {
fse.copySync(packageBakFile, packageFile)
if (onlyRecovery !== true) {
process.exit()
}
}
process.on('exit', recoveryPackage);
process.on('SIGINT', recoveryPackage);
process.on('SIGHUP', recoveryPackage);
// package.json Generated
const appConfig = require('./package.json')
let appName = utils.getDomain(data.url)
if (appName === "public") appName = "DooTask"
appConfig.name = data.name;
appConfig.version = config.version;
appConfig.appId = data.id;
appConfig.build.appId = data.id;
appConfig.build.artifactName = appName + "-v${version}-${os}-${arch}.${ext}";
appConfig.build.nsis.artifactName = appName + "-v${version}-${os}-${arch}.${ext}";
// changelog
appConfig.build.releaseInfo.releaseNotes = changeLog()
if (!release) {
appConfig.build.releaseInfo.releaseNotes = appConfig.build.releaseInfo.releaseNotes.replace(`## [${config.version}]`, `## [${config.version}-Silence]`)
}
// notarize
if (notarize && APPLEID && APPLEIDPASS) {
appConfig.build.afterSign = "./notarize.js"
}
// archs
if (archs.length > 0) {
appConfig.build.mac.target = appConfig.build.mac.target.map(target => {
if (target.arch) target.arch = target.arch.filter(arch => archs.includes(arch))
return target
})
appConfig.build.win.target = appConfig.build.win.target.map(target => {
if (target.arch) target.arch = target.arch.filter(arch => archs.includes(arch))
return target
})
}
// GitHub (build and publish)
if (publish === true && GITHUB_TOKEN && utils.strExists(GITHUB_REPOSITORY, "/")) {
const repository = GITHUB_REPOSITORY.split("/")
appConfig.build.publish = {
"releaseType": "release",
"provider": "github",
"owner": repository[0],
"repo": repository[1]
}
appConfig.build.directories.output = `${output}-github`;
fs.writeFileSync(packageFile, JSON.stringify(appConfig, null, 4), 'utf8');
child_process.execSync(`npm run ${platform}-publish`, {stdio: "inherit", cwd: "electron"});
}
// generic (build or publish) —— 有 R2_PUBLIC_URL 时自动更新源指向 R2 release/
appConfig.build.publish = r2.R2_PUBLIC_URL
? { provider: 'generic', url: `${r2.R2_PUBLIC_URL.replace(/\/+$/, '')}/release` }
: data.publish
appConfig.build.directories.output = `${output}-generic`;
fs.writeFileSync(packageFile, JSON.stringify(appConfig, null, 4), 'utf8');
child_process.execSync(`npm run ${platform}`, {stdio: "inherit", cwd: "electron"});
if (publish === true && r2.r2Configured()) {
const client = r2.createR2Client()
const outputDir = path.resolve(__dirname, appConfig.build.directories.output)
if (fs.existsSync(outputDir)) {
const files = fs.readdirSync(outputDir)
for (const filename of files) {
const localFile = path.join(outputDir, filename)
if (!fs.statSync(localFile).isFile()) continue
await uploadDraftFile(client, localFile, config.version)
}
}
}
// package.json Recovery
recoveryPackage(true)
}
/** ************************************************************************************/
/** ************************************************************************************/
/** ************************************************************************************/
if (["dev"].includes(argv[2])) {
// 开发模式
fs.writeFileSync(devloadCachePath, utils.formatUrl("localhost:" + env.parsed.APP_PORT), 'utf8');
child_process.spawn("npx", ["vite", "--", "fromcmd", "electronDev"], {stdio: "inherit"});
child_process.spawn("npm", ["run", "start-quiet"], {stdio: "inherit", cwd: "electron"});
} else if (argv[2] === 'app-expo') {
if ((argv[3] && argv[3] !== 'build') || argv.length > 4) {
console.error("用法: node ./electron/build.js app-expo [build]");
process.exit(1);
}
if (!fs.existsSync(path.resolve(__dirname, "../resources/app/app.config.ts"))) {
console.error("resources/app 未找到,请先初始化 Expo 子模块");
process.exit(1);
}
startBuild({
name: 'App',
id: 'app-expo',
// public 是前端“尚未设置服务器”的占位域名,不是静态资源的 origin。
// electronBuild 的 ./ 资源路径在 localhost:22223/ 下加载,路由自动使用 hash。
url: 'http://public/',
configure: {
platform: '',
archs: [],
publish: false,
release: true,
notarize: false,
}
}).catch(error => {
console.error(error.message || error);
process.exitCode = 1;
});
} else if (["android-upload"].includes(argv[2])) {
// 上传安卓文件到 R2 draft(GitHub Actions):参数为 APK 所在目录(app-build.yml 产出的 app-android)
(async () => {
if (!r2.r2Configured()) {
console.error("缺少 R2_* 环境变量(R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY/R2_ENDPOINT/R2_BUCKET)")
process.exit(1)
}
const client = r2.createR2Client()
const releaseDir = path.resolve(argv[3] || path.resolve(__dirname, "../resources/app/android/app/build/outputs/apk/release"));
if (!fs.existsSync(releaseDir)) {
console.error("发布文件未找到")
process.exit(1)
}
const files = fs.readdirSync(releaseDir)
for (const filename of files) {
const localFile = path.join(releaseDir, filename)
if (/\.apk$/.test(filename) && fs.existsSync(localFile) && fs.statSync(localFile).isFile()) {
await uploadDraftFile(client, localFile, config.version)
}
}
})().catch(err => {
console.error(err.message || err)
process.exit(1)
})
} else if (["release"].includes(argv[2])) {
// R2 内提升:draft/<version> → release/(当前版扁平,旧版归档 release/<prev>/)
(async () => {
if (!r2.r2Configured()) {
console.error("缺少 R2_* 环境变量")
process.exit(1)
}
const client = r2.createR2Client()
const version = config.version
const draftPrefix = `draft/${version}/`
const draftKeys = await r2.listKeys(client, draftPrefix)
if (!draftKeys.length) {
console.error(`draft/${version}/ 为空,无法发布`)
process.exit(1)
}
const names = draftKeys.map(k => k.slice(draftPrefix.length))
// 读 manifest 取上一发布版
const manifest = JSON.parse(await r2.getText(client, 'manifest.json') || '{"draft":null,"release":null}')
const prev = manifest.release
// 1. 归档上一版扁平文件 → release/<prev>/
if (prev && prev !== version) {
const prevRootKeys = await r2.listKeys(client, 'release/', '/')
for (const key of prevRootKeys) {
const name = key.slice('release/'.length)
await r2.copyObject(client, key, `release/${prev}/${name}`)
}
}
// 2. 清空扁平根层(仅根层对象,版本归档子目录不动)
const rootKeys = await r2.listKeys(client, 'release/', '/')
await r2.deleteKeys(client, rootKeys)
// 3. 铺新扁平:安装包/blockmap/zip 先,latest*.yml 最后
const ymls = names.filter(n => /\.ya?ml$/i.test(n))
const others = names.filter(n => !/\.ya?ml$/i.test(n))
for (const name of others) await r2.copyObject(client, `${draftPrefix}${name}`, `release/${name}`)
for (const name of ymls) await r2.copyObject(client, `${draftPrefix}${name}`, `release/${name}`)
// 4. 下载索引
const index = buildReleaseIndex(names)
await r2.putText(client, 'release/index.json', JSON.stringify({ version, files: index }, null, 2))
// 5. 更新 manifest,清理 draft
await r2.putText(client, 'manifest.json', JSON.stringify({ draft: null, release: version }, null, 2))
await r2.deleteKeys(client, draftKeys)
console.log(`Release published: v${version}`)
})().catch(err => {
console.error(err.message || err)
process.exit(1)
})
} else if (["upload-changelog"].includes(argv[2])) {
// 上传 changelog 到 R2(GitHub Actions)
(async () => {
if (!r2.r2Configured()) {
console.error("缺少 R2_* 环境变量")
process.exit(1)
}
const changelogPath = path.resolve(__dirname, "../CHANGELOG.md")
if (!fs.existsSync(changelogPath)) {
console.error("CHANGELOG.md 未找到")
process.exit(1)
}
const client = r2.createR2Client()
const content = fs.readFileSync(changelogPath, 'utf8')
await r2.putText(client, 'changelog.md', content)
console.log('Changelog uploaded')
})().catch(err => {
console.error(err.message || err)
process.exit(1)
})
} else if (["all", "win", "mac"].includes(argv[2])) {
// 自动编译(GitHub Actions)
platforms.filter(p => {
return argv[2] === "all" || p.indexOf(argv[2]) !== -1
}).forEach(async platform => {
for (const data of config.app) {
data.configure = {
platform,
archs: architectures,
publish: true,
release: true,
notarize: false,
};
await startBuild(data);
}
});
} else {
// 手编译(默认)
let cachedConfig = {};
try {
const buildConfigPath = path.join(__dirname, '.build');
if (fs.existsSync(buildConfigPath)) {
const configContent = fs.readFileSync(buildConfigPath, 'utf-8');
if (configContent.trim()) {
cachedConfig = JSON.parse(configContent);
}
}
} catch (error) {
console.warn('读取缓存配置失败:', error.message);
}
const questions = [
{
type: 'checkbox',
name: 'platform',
message: "选择编译系统",
choices: [
{
name: "MacOS",
value: platforms[0],
},
{
name: "Windows",
value: platforms[1]
}
],
default: (cachedConfig && cachedConfig.platform) || [],
validate: (answer) => {
if (answer.length < 1) {
return '请至少选择一个系统';
}
return true;
}
},
{
type: 'checkbox',
name: 'arch',
message: "选择系统架构",
choices: ({platform}) => {
const array = [
{
name: "arm64",
value: architectures[0],
},
{
name: "x64",
value: architectures[1]
}
]
if (platform.find(item => item === 'build-mac')) {
array.push({
name: "通用" + (platform.length > 1 ? " (仅MacOS)" : ""),
value: 'universal'
})
}
return array;
},
default: (cachedConfig && cachedConfig.arch) || [],
validate: (answer) => {
if (answer.length < 1) {
return '请至少选择一个架构';
}
return true;
}
},
{
type: 'list',
name: 'publish',
message: "选择是否发布",
choices: [{
name: "否",
value: false
}, {
name: "是",
value: true
}],
default: (cachedConfig && cachedConfig.publish !== undefined) ?
(cachedConfig.publish ? 1 : 0) : 0
},
{
type: 'list',
name: 'release',
message: "选择升级方式",
when: ({publish}) => publish,
choices: [{
name: "弹出提示",
value: true
}, {
name: "静默",
value: false
}],
default: (cachedConfig && cachedConfig.release !== undefined) ?
(cachedConfig.release ? 0 : 1) : 0
},
{
type: 'list',
name: 'notarize',
message: ({platform}) => platform.length > 1 ? "选择是否公证(仅MacOS)" : "选择是否公证",
when: ({platform}) => platform.find(item => item === 'build-mac'),
choices: [{
name: "否",
value: false
}, {
name: "是",
value: true
}],
default: (cachedConfig && cachedConfig.notarize !== undefined) ?
(cachedConfig.notarize ? 1 : 0) : 0
}
];
// 开始提问
const prompt = inquirer.createPromptModule();
prompt(questions)
.then(async answers => {
answers = Object.assign({
release: false,
notarize: false
}, answers);
// 缓存当前配置
try {
fs.writeFileSync(path.join(__dirname, '.build'), JSON.stringify(answers, null, 4), 'utf-8');
} catch (error) {
console.warn('保存配置缓存失败:', error.message);
}
// 发布判断环境变量
if (answers.publish) {
if (!r2.r2Configured() && !(GITHUB_TOKEN && utils.strExists(GITHUB_REPOSITORY, "/"))) {
console.error("发布需要 R2_* 或 GITHUB_TOKEN + GITHUB_REPOSITORY, 请检查环境变量!");
process.exit()
}
}
// 公证判断环境变量
if (answers.notarize === true) {
if (!APPLEID || !APPLEIDPASS) {
console.error("公证需要 Apple ID 和 Apple ID 密码, 请检查环境变量!");
process.exit()
}
}
// 开始构建
for (const platform of answers.platform) {
for (const data of config.app) {
data.configure = {
platform,
archs: answers.arch,
publish: answers.publish,
release: answers.release,
notarize: answers.notarize
};
await startBuild(data);
}
}
})
.catch(_ => { });
}