355 lines
10 KiB
TypeScript
355 lines
10 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import crypto from "node:crypto";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
const root = path.resolve(import.meta.dir, "..");
|
|
const krita = path.join(root, "krita");
|
|
const deps = path.join(root, "third_party", "krita-deps-management");
|
|
const ciUtilities = path.join(root, "third_party", "krita-ci-utilities");
|
|
const kdeCiUtilities = path.join(root, "third_party", "kde-ci-utilities");
|
|
const notaryService = path.join(root, "third_party", "ci-notary-service");
|
|
const venv = path.join(root, ".venv-krita-build");
|
|
const python =
|
|
process.platform === "win32"
|
|
? path.join(venv, "Scripts", "python.exe")
|
|
: path.join(venv, "bin", "python");
|
|
|
|
function run(
|
|
command: string,
|
|
args: string[],
|
|
cwd = root,
|
|
env = process.env,
|
|
): void {
|
|
const result = spawnSync(command, args, {
|
|
cwd,
|
|
env,
|
|
stdio: "inherit",
|
|
});
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0)
|
|
throw new Error(`${command} ${args.join(" ")} failed`);
|
|
}
|
|
|
|
function requirePath(value: string | undefined, name: string): string {
|
|
if (!value)
|
|
throw new Error(`Set ${name} to the installed Android toolchain path`);
|
|
if (!fs.existsSync(value))
|
|
throw new Error(`${name} does not exist: ${value}`);
|
|
return path.resolve(value);
|
|
}
|
|
|
|
function resolveAndroidNdk(value: string | undefined, sdk: string): string {
|
|
const roots = [value, path.join(sdk, "ndk")].filter((item): item is string =>
|
|
Boolean(item),
|
|
);
|
|
const candidates = new Set<string>();
|
|
for (const root of roots) {
|
|
if (!fs.existsSync(root)) continue;
|
|
candidates.add(path.resolve(root));
|
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
if (entry.isDirectory())
|
|
candidates.add(path.join(path.resolve(root), entry.name));
|
|
}
|
|
}
|
|
for (const candidate of candidates) {
|
|
const toolchain = path.join(
|
|
candidate,
|
|
"build",
|
|
"cmake",
|
|
"android.toolchain.cmake",
|
|
);
|
|
const cpuFeatures = path.join(
|
|
candidate,
|
|
"sources",
|
|
"android",
|
|
"cpufeatures",
|
|
);
|
|
if (fs.existsSync(toolchain) && fs.existsSync(cpuFeatures))
|
|
return candidate;
|
|
}
|
|
throw new Error(
|
|
"Set ANDROID_NDK_ROOT to an NDK directory, or set ANDROID_SDK_ROOT to an SDK containing ndk/<version>",
|
|
);
|
|
}
|
|
|
|
function readJavaMajor(javaHome: string): number | undefined {
|
|
const release = path.join(javaHome, "release");
|
|
if (!fs.existsSync(release)) return undefined;
|
|
const match = fs.readFileSync(release, "utf8").match(/^JAVA_VERSION="(\d+)/m);
|
|
return match ? Number(match[1]) : undefined;
|
|
}
|
|
|
|
function resolveBuildJavaHome(): string {
|
|
const requested = process.env.KRITA_JAVA_HOME;
|
|
const roots = requested
|
|
? [requested]
|
|
: [
|
|
process.env.JAVA_HOME ?? "",
|
|
"/usr/lib/jvm/java-21-openjdk",
|
|
"/usr/lib/jvm/java-17-openjdk",
|
|
"/usr/lib/jvm",
|
|
];
|
|
const candidates = new Set<string>();
|
|
for (const root of roots) {
|
|
if (!root || !fs.existsSync(root)) continue;
|
|
candidates.add(path.resolve(root));
|
|
if (fs.statSync(root).isDirectory()) {
|
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
if (entry.isDirectory())
|
|
candidates.add(path.join(path.resolve(root), entry.name));
|
|
}
|
|
}
|
|
}
|
|
for (const candidate of candidates) {
|
|
const major = readJavaMajor(candidate);
|
|
if (
|
|
major !== undefined &&
|
|
major >= 17 &&
|
|
major <= 24 &&
|
|
fs.existsSync(path.join(candidate, "bin", "java"))
|
|
) {
|
|
return candidate;
|
|
}
|
|
}
|
|
throw new Error(
|
|
"Install JDK 17 through 24, or set KRITA_JAVA_HOME to a supported JDK",
|
|
);
|
|
}
|
|
|
|
function readEnvironmentFile(file: string): Record<string, string> {
|
|
return Object.fromEntries(
|
|
fs
|
|
.readFileSync(file, "utf8")
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => line.split("=", 2) as [string, string]),
|
|
);
|
|
}
|
|
|
|
function releaseSigning(javaHome: string): Record<string, string> {
|
|
const signingDir = path.join(root, "build", "signing");
|
|
const keystore = path.join(signingDir, "krita-eink-release.jks");
|
|
const environmentFile = path.join(signingDir, "krita-eink-release.env");
|
|
fs.mkdirSync(signingDir, { recursive: true, mode: 0o700 });
|
|
if (!fs.existsSync(keystore)) {
|
|
const password = crypto.randomBytes(32).toString("base64url");
|
|
const values = {
|
|
MISSTRING_RELEASE_STORE_FILE: keystore,
|
|
MISSTRING_RELEASE_STORE_PASSWORD: password,
|
|
MISSTRING_RELEASE_KEY_ALIAS: "krita-eink",
|
|
MISSTRING_RELEASE_KEY_PASSWORD: password,
|
|
};
|
|
run(path.join(javaHome, "bin", "keytool"), [
|
|
"-genkeypair",
|
|
"-keystore",
|
|
keystore,
|
|
"-storetype",
|
|
"PKCS12",
|
|
"-storepass",
|
|
password,
|
|
"-alias",
|
|
values.MISSTRING_RELEASE_KEY_ALIAS,
|
|
"-keypass",
|
|
password,
|
|
"-keyalg",
|
|
"RSA",
|
|
"-keysize",
|
|
"4096",
|
|
"-validity",
|
|
"36500",
|
|
"-dname",
|
|
"CN=MiString Krita E Ink,O=rip.crit.ms,C=US",
|
|
]);
|
|
fs.writeFileSync(
|
|
environmentFile,
|
|
`${Object.entries(values)
|
|
.map(([key, value]) => `${key}=${value}`)
|
|
.join("\n")}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
console.log(`[ok] Created local release keystore at ${keystore}`);
|
|
}
|
|
if (!fs.existsSync(environmentFile))
|
|
throw new Error(`Missing signing credentials: ${environmentFile}`);
|
|
return readEnvironmentFile(environmentFile);
|
|
}
|
|
|
|
function removeManagedLink(
|
|
directory: string,
|
|
name: string,
|
|
target: string,
|
|
): void {
|
|
const link = path.join(directory, name);
|
|
if (!fs.existsSync(link) && !fs.lstatSync(link, { throwIfNoEntry: false }))
|
|
return;
|
|
const metadata = fs.lstatSync(link);
|
|
if (
|
|
!metadata.isSymbolicLink() ||
|
|
fs.realpathSync(link) !== fs.realpathSync(target)
|
|
) {
|
|
throw new Error(`${link} exists and is not a generated compatibility link`);
|
|
}
|
|
fs.unlinkSync(link);
|
|
}
|
|
|
|
function sync(): void {
|
|
run("git", ["submodule", "update", "--init", "--recursive"]);
|
|
for (const source of [
|
|
krita,
|
|
deps,
|
|
ciUtilities,
|
|
kdeCiUtilities,
|
|
notaryService,
|
|
]) {
|
|
if (!fs.existsSync(path.join(source, ".git"))) {
|
|
throw new Error(`Missing submodule checkout: ${source}`);
|
|
}
|
|
}
|
|
removeManagedLink(krita, "krita-deps-management", deps);
|
|
removeManagedLink(deps, "ci-utilities", ciUtilities);
|
|
removeManagedLink(krita, "upstream-ci-utilities", kdeCiUtilities);
|
|
removeManagedLink(krita, "ci-notary-service", notaryService);
|
|
run("bun", ["run", "kr"]);
|
|
console.log(
|
|
"[ok] Source submodules are initialized and the Xiaomi E Ink patch is applied",
|
|
);
|
|
}
|
|
|
|
function bootstrap(): void {
|
|
sync();
|
|
if (!fs.existsSync(python))
|
|
run("python3", ["-m", "venv", venv, "--upgrade-deps"]);
|
|
run(python, [
|
|
"-m",
|
|
"pip",
|
|
"install",
|
|
"-r",
|
|
path.join(deps, "requirements.txt"),
|
|
]);
|
|
console.log("[ok] Krita dependency manager Python environment is ready");
|
|
}
|
|
|
|
function generateDeps(): void {
|
|
if (!fs.existsSync(python)) {
|
|
throw new Error(
|
|
"Run `bun run krita:bootstrap` before generating the dependency graph",
|
|
);
|
|
}
|
|
const output = path.join(root, "build", "krita", ".kde-ci.generated.yml");
|
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
run(
|
|
python,
|
|
[
|
|
path.join(deps, "tools", "generate-deps-file.py"),
|
|
"-s",
|
|
path.join(deps, "latest", "krita-deps.yml"),
|
|
"-o",
|
|
output,
|
|
],
|
|
krita,
|
|
{ ...process.env, PYTHONDONTWRITEBYTECODE: "1" },
|
|
);
|
|
console.log(`[ok] Generated ${output}`);
|
|
}
|
|
|
|
function build(release = false): void {
|
|
sync();
|
|
generateDeps();
|
|
const sdk = requirePath(process.env.ANDROID_SDK_ROOT, "ANDROID_SDK_ROOT");
|
|
const ndk = resolveAndroidNdk(process.env.ANDROID_NDK_ROOT, sdk);
|
|
const javaHome = resolveBuildJavaHome();
|
|
const signing = release ? releaseSigning(javaHome) : {};
|
|
const localBuildRoot = path.join(root, "build", "krita");
|
|
const localCache =
|
|
process.env.KDECI_CACHE_PATH ?? path.join(localBuildRoot, "cache");
|
|
const localWork =
|
|
process.env.KDECI_WORKDIR_PATH ?? path.join(localBuildRoot, "work");
|
|
const localCcache =
|
|
process.env.KDECI_CC_CACHE ?? path.join(localBuildRoot, "ccache");
|
|
const xiaomiEink = process.env.KRITA_XIAOMI_EINK ?? "1";
|
|
const xiaomiEinkEnabled = ["1", "ON", "true", "TRUE"].includes(xiaomiEink);
|
|
for (const directory of [localCache, localWork, localCcache]) {
|
|
fs.mkdirSync(directory, { recursive: true });
|
|
}
|
|
const environment = {
|
|
...process.env,
|
|
KRITA_EINK_SLIM: "1",
|
|
KRITA_XIAOMI_EINK: xiaomiEink,
|
|
UNOTE_SOURCE_DIR: path.join(root, "unote", "src", "main", "java"),
|
|
UNOTE_PLUS_SOURCE_DIR: path.join(root, "unote-plus", "src", "main", "java"),
|
|
KRITACI_ANDROID_PACKAGE_TYPE: release ? "release" : "debug",
|
|
KRITACI_ANDROID_RELEASE_MODE: release ? "1" : "",
|
|
PYTHONDONTWRITEBYTECODE: "1",
|
|
JAVA_HOME: javaHome,
|
|
PATH: `${path.join(javaHome, "bin")}:${process.env.PATH ?? ""}`,
|
|
KDECI_REPO_METADATA_PATH: path.join(deps, "repo-metadata"),
|
|
KDECI_ANDROID_SDK_ROOT: sdk,
|
|
KDECI_ANDROID_NDK_ROOT: ndk,
|
|
KDECI_ANDROID_ABI: "arm64-v8a",
|
|
KDECI_BUILD_TYPE: "Release",
|
|
KDECI_SKIP_ECM_ANDROID_TOOLCHAIN: "True",
|
|
KDECI_CACHE_PATH: localCache,
|
|
KDECI_WORKDIR_PATH: localWork,
|
|
KDECI_CC_CACHE: localCcache,
|
|
KDECI_GITLAB_SERVER:
|
|
process.env.KDECI_GITLAB_SERVER ?? "https://invent.kde.org/",
|
|
KDECI_PACKAGE_PROJECT:
|
|
process.env.KDECI_PACKAGE_PROJECT ??
|
|
"teams/ci-artifacts/krita-android-arm64-v8a",
|
|
KDECI_COMPRESS_PACKAGES_ON_DOWNLOAD: "False",
|
|
...signing,
|
|
CMAKE_TOOLCHAIN_FILE: path.join(
|
|
deps,
|
|
"tools",
|
|
"android-toolchain-krita.cmake",
|
|
),
|
|
KDECI_EXTRA_CMAKE_ARGS: `-DKRITA_EINK_SLIM=ON -DKRITA_XIAOMI_EINK=${xiaomiEinkEnabled ? "ON" : "OFF"} -DBUILD_TESTING=OFF -DENABLE_UPDATERS=OFF`,
|
|
};
|
|
const sourceConfig = path.join(krita, ".kde-ci.yml");
|
|
const generatedConfig = path.join(
|
|
root,
|
|
"build",
|
|
"krita",
|
|
".kde-ci.generated.yml",
|
|
);
|
|
const originalConfig = fs.readFileSync(sourceConfig);
|
|
fs.copyFileSync(generatedConfig, sourceConfig);
|
|
try {
|
|
run(
|
|
python,
|
|
[
|
|
path.join(ciUtilities, "run-ci-build.py"),
|
|
"--project",
|
|
"krita",
|
|
"--branch",
|
|
"master",
|
|
"--platform",
|
|
"Android/arm64-v8a/Qt5/Shared",
|
|
"--only-build",
|
|
"--skip-publishing",
|
|
],
|
|
krita,
|
|
environment,
|
|
);
|
|
run(
|
|
python,
|
|
["build-tools/ci-scripts/build-android-package.py"],
|
|
krita,
|
|
environment,
|
|
);
|
|
} finally {
|
|
fs.writeFileSync(sourceConfig, originalConfig);
|
|
}
|
|
}
|
|
|
|
const command = process.argv[2] ?? "sync";
|
|
if (command === "sync") sync();
|
|
else if (command === "bootstrap") bootstrap();
|
|
else if (command === "deps") generateDeps();
|
|
else if (command === "build") build();
|
|
else if (command === "release") build(true);
|
|
else
|
|
throw new Error(
|
|
"Usage: krita-unified-build.ts [sync|bootstrap|deps|build|release]",
|
|
);
|