Fix ARMSX2 iOS source generation reliability

This commit is contained in:
Jeen
2026-06-05 23:42:56 +02:00
parent 43ae01ad94
commit bff70dea43
15 changed files with 531 additions and 69 deletions
+3 -1
View File
@@ -4,4 +4,6 @@ import { fileURLToPath } from "node:url";
export const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
export const sourceIdentifier = "com.j1coding.armsx2.ios";
export const bundleIdentifier = "com.armsx2.ios";
export const defaultBaseUrl = "https://j1coding.github.io/Armsx2-Repo";
export const canonicalBaseUrl = "https://ios.armsx2.net";
export const canonicalSourceUrl = `${canonicalBaseUrl}/apps.json`;
export const defaultBaseUrl = canonicalBaseUrl;
+2 -2
View File
@@ -116,8 +116,8 @@ const runGenerator = async () => {
await writeGeneratedTargets(targets, generated);
if (defaultOutputPaths(generatorOptions)) {
for (const screenshotFile of generated.screenshotFiles) {
await mirrorPublicAsset(screenshotFile);
for (const assetFile of generated.assetFiles) {
await mirrorPublicAsset(assetFile);
}
}
+1 -1
View File
@@ -64,7 +64,7 @@ const releaseMatchesManifest = (githubRelease, manifest) => {
githubRelease.name,
].filter(Boolean).map(String);
const versionPattern = new RegExp(`(^|[^0-9])v?${escapedRegExp(manifest.version)}([^0-9]|$)`, "iu");
const versionPattern = new RegExp(`(^|[^0-9])v?${escapedRegExp(manifest.version)}(?=$|[^0-9.])`, "iu");
return releaseFields.some((releaseField) => versionPattern.test(releaseField));
};
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env node
import { canonicalBaseUrl, canonicalSourceUrl } from "./constants.js";
const expectedVersion = "1.3";
const liveUrls = [
canonicalSourceUrl,
`${canonicalBaseUrl}/checksums.json`,
`${canonicalBaseUrl}/ipas/ARMSX2-iOS-${expectedVersion}.ipa`,
`${canonicalBaseUrl}/assets/icon.png`,
];
const headCheck = async (url) => {
const response = await fetch(url, { method: "HEAD", redirect: "follow" });
return {
url,
status: response.status,
ok: response.ok,
finalUrl: response.url,
contentType: response.headers.get("content-type"),
contentLength: response.headers.get("content-length"),
};
};
const jsonDocument = async (url) => {
const response = await fetch(url, {
headers: { Accept: "application/json" },
redirect: "follow",
});
if (!response.ok) {
throw new Error(`${url} returned ${response.status}.`);
}
return response.json();
};
const runSmokeCheck = async () => {
const results = [];
for (const liveUrl of liveUrls) {
results.push(await headCheck(liveUrl));
}
const sourceJson = await jsonDocument(canonicalSourceUrl);
const sourceVersion = sourceJson.apps?.[0]?.versions?.[0]?.version;
const downloadURL = sourceJson.apps?.[0]?.versions?.[0]?.downloadURL;
console.log(JSON.stringify({
expectedVersion,
sourceVersion,
downloadURL,
checks: results,
}, null, 2));
const failures = results
.filter((result) => !result.ok)
.map((result) => `${result.url} returned ${result.status}`);
if (sourceJson.sourceURL !== canonicalSourceUrl) {
failures.push(`Live sourceURL is ${sourceJson.sourceURL}; expected ${canonicalSourceUrl}.`);
}
if (sourceVersion !== expectedVersion) {
failures.push(`Live version is ${sourceVersion}; expected ${expectedVersion}.`);
}
if (downloadURL !== `${canonicalBaseUrl}/ipas/ARMSX2-iOS-${expectedVersion}.ipa`) {
failures.push(`Live downloadURL is ${downloadURL}; expected ${canonicalBaseUrl}/ipas/ARMSX2-iOS-${expectedVersion}.ipa.`);
}
if (failures.length > 0) {
throw new Error(failures.join("\n"));
}
};
try {
await runSmokeCheck();
} catch (smokeError) {
console.error(smokeError instanceof Error ? smokeError.message : smokeError);
process.exitCode = 1;
}
+3 -1
View File
@@ -38,6 +38,7 @@ const storeMetadata = async (generatorOptions) => {
};
const imageExtensions = new Set([".png", ".jpg", ".jpeg", ".webp"]);
const iconFile = "assets/icon.png";
const screenshotDirectory = (generatorOptions, metadataPayload) =>
generatorOptions.screenshotDirectory
@@ -97,7 +98,7 @@ const sourceApp = (ipaFileManifests, generatorOptions, metadataPayload, screensh
subtitle: metadataPayload.app.subtitle ?? "Modern PlayStation 2 emulation for iOS.",
localizedDescription: metadataPayload.app.localizedDescription
?? "ARMSX2 brings PlayStation 2 emulation to iOS devices. Based on the open-source PCSX2 project, this ARM64-focused iOS build helps you revisit and preserve your own legally obtained PS2 game library on modern mobile hardware.",
iconURL: publicAssetUrl(generatorOptions.baseUrl, "assets/icon.png"),
iconURL: publicAssetUrl(generatorOptions.baseUrl, iconFile),
screenshotURLs: screenshotFiles.map((screenshotFile) => publicAssetUrl(generatorOptions.baseUrl, screenshotFile)),
tintColor: "#2F6FAD",
versions: ipaFileManifests.map(sourceVersion),
@@ -164,6 +165,7 @@ export const generatedBuffers = async (generatorOptions) => {
return {
source: jsonBuffer(sourcePayload(ipaFileManifests, generatorOptions, metadataPayload, screenshotFiles)),
checksums: jsonBuffer(checksumPayload(ipaFileManifests, generatorOptions)),
assetFiles: [iconFile, ...screenshotFiles],
screenshotFiles,
ipaCount: ipaFileManifests.length,
};
+134 -13
View File
@@ -19,6 +19,7 @@ import { promisify } from "node:util";
import { parseOptions, setOptionFlag, setOptionValue } from "./cli.js";
import { defaultBaseUrl, repositoryRoot } from "./constants.js";
import { ipaFileManifest } from "./ipa-metadata.js";
const execFileAsync = promisify(execFile);
const generatorPath = resolve(repositoryRoot, "scripts/generate-source.js");
@@ -113,25 +114,82 @@ const githubReleases = async (repositoryName) => {
const releaseTimestamp = (githubRelease) =>
String(githubRelease.published_at ?? githubRelease.created_at ?? "");
const versionFromIosRelease = (githubRelease) => {
const releaseText = `${githubRelease.tag_name ?? ""} ${githubRelease.name ?? ""}`;
return releaseText.match(/\biosv?(\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?)/iu)?.[1]
?? releaseText.match(/\barmsx2-ios\D+(\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?)/iu)?.[1]
?? null;
};
const versionFromIosAsset = (releaseAsset) =>
releaseAsset.name?.match(/^ARMSX2-iOS-v?(\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?)\.ipa$/iu)?.[1]
?? null;
const candidateScore = (candidate) => {
let score = 0;
if (candidate.releaseVersion) {
score += 100;
}
if (candidate.assetVersion) {
score += 50;
}
if (candidate.releaseVersion && candidate.releaseVersion === candidate.assetVersion) {
score += 25;
}
if (/^ARMSX2-iOS-v?\d/iu.test(candidate.releaseAsset.name ?? "")) {
score += 10;
}
return score;
};
const releaseCandidates = (releases, syncOptions) =>
releases
.filter((githubRelease) => !githubRelease.draft)
.filter((githubRelease) => syncOptions.includePrereleases || !githubRelease.prerelease)
.flatMap((githubRelease) =>
(githubRelease.assets ?? [])
.flatMap((githubRelease) => {
const releaseVersion = versionFromIosRelease(githubRelease);
return (githubRelease.assets ?? [])
.filter((releaseAsset) => releaseAsset.name?.toLowerCase().endsWith(".ipa"))
.map((releaseAsset) => ({ githubRelease, releaseAsset })),
)
.sort((leftCandidate, rightCandidate) =>
releaseTimestamp(rightCandidate.githubRelease).localeCompare(releaseTimestamp(leftCandidate.githubRelease)),
);
.map((releaseAsset) => ({
githubRelease,
releaseAsset,
releaseVersion,
assetVersion: versionFromIosAsset(releaseAsset),
}));
})
.filter((candidate) => candidate.releaseVersion || candidate.assetVersion)
.map((candidate) => ({ ...candidate, score: candidateScore(candidate) }))
.sort((leftCandidate, rightCandidate) => {
const scoreDelta = rightCandidate.score - leftCandidate.score;
if (scoreDelta !== 0) {
return scoreDelta;
}
return releaseTimestamp(rightCandidate.githubRelease).localeCompare(releaseTimestamp(leftCandidate.githubRelease));
});
const selectedReleaseCandidate = (candidates) => {
const iosCandidate = candidates.find(({ githubRelease, releaseAsset }) =>
`${githubRelease.tag_name ?? ""} ${githubRelease.name ?? ""} ${releaseAsset.name ?? ""}`.match(/ios/i),
);
const [bestCandidate, nextCandidate] = candidates;
return iosCandidate ?? candidates[0] ?? null;
if (!bestCandidate) {
return null;
}
if (nextCandidate && nextCandidate.score === bestCandidate.score
&& releaseTimestamp(nextCandidate.githubRelease) === releaseTimestamp(bestCandidate.githubRelease)) {
throw new UpstreamSyncError(
`Multiple equally good iOS IPA assets were found: ${bestCandidate.releaseAsset.name}, ${nextCandidate.releaseAsset.name}.`,
);
}
return bestCandidate;
};
const downloadAssetToFile = async (releaseAsset, outputPath) => {
@@ -187,6 +245,35 @@ const writeGithubOutput = async (outputPayload) => {
await writeFile(process.env.GITHUB_OUTPUT, `${outputLines}\n`, { flag: "a" });
};
const writeGithubSummary = async (summaryLines) => {
if (!process.env.GITHUB_STEP_SUMMARY) {
return;
}
await writeFile(process.env.GITHUB_STEP_SUMMARY, `${summaryLines.join("\n")}\n`, { flag: "a" });
};
const syncSummaryLines = ({
changed,
repositoryName,
githubRelease,
releaseAsset,
manifest,
ipaSha256,
}) => [
"## Source update summary",
"",
`- Upstream repository: ${repositoryName}`,
`- Selected release: ${githubRelease.tag_name ?? githubRelease.name ?? "unknown"}`,
`- Selected asset: ${releaseAsset.name}`,
`- Detected app version: ${manifest.version}`,
`- Bundle identifier: ${manifest.bundleIdentifier}`,
`- File size: ${manifest.size}`,
`- SHA-256: ${ipaSha256}`,
`- IPA changed: ${changed ? "yes" : "no"}`,
"",
];
const runNodeScript = async (scriptPath, scriptArguments, extraEnvironment = {}) => {
await execFileAsync(process.execPath, [scriptPath, ...scriptArguments], {
cwd: repositoryRoot,
@@ -219,6 +306,8 @@ const validateCandidate = async (temporaryDirectory, candidateIpaDirectory, sync
candidateSourcePath,
"--checksums",
candidateChecksumsPath,
"--ipa-dir",
candidateIpaDirectory,
"--skip-offline-fallback",
"--skip-legacy-purge",
]);
@@ -253,6 +342,7 @@ const syncLatestIpa = async () => {
const candidateIpaPath = join(temporaryDirectory, "ipas", assetName);
const versionLabel = githubRelease.tag_name ?? githubRelease.name ?? assetName;
const ipaSha256 = await downloadAssetToFile(releaseAsset, candidateIpaPath);
const candidateManifest = await ipaFileManifest(candidateIpaPath, syncOptions);
if (await checksumExists(syncOptions, ipaSha256)) {
await writeGithubOutput({
@@ -261,7 +351,22 @@ const syncLatestIpa = async () => {
version: versionLabel,
sha256: ipaSha256,
});
console.log(`No update: ${assetName} is already published (${ipaSha256}).`);
await writeGithubSummary(syncSummaryLines({
changed: false,
repositoryName,
githubRelease,
releaseAsset,
manifest: candidateManifest,
ipaSha256,
}));
console.log([
`No update: ${assetName} is already published.`,
`Selected release: ${versionLabel}`,
`Detected app version: ${candidateManifest.version}`,
`Bundle identifier: ${candidateManifest.bundleIdentifier}`,
`File size: ${candidateManifest.size}`,
`SHA-256: ${ipaSha256}`,
].join("\n"));
return;
}
@@ -274,8 +379,24 @@ const syncLatestIpa = async () => {
version: versionLabel,
sha256: ipaSha256,
});
await writeGithubSummary(syncSummaryLines({
changed: true,
repositoryName,
githubRelease,
releaseAsset,
manifest: candidateManifest,
ipaSha256,
}));
console.log(`Published ${publishedIpaPath} from ${repositoryName} (${ipaSha256}).`);
console.log([
`Published ${publishedIpaPath} from ${repositoryName}.`,
`Selected release: ${versionLabel}`,
`Selected asset: ${releaseAsset.name}`,
`Detected app version: ${candidateManifest.version}`,
`Bundle identifier: ${candidateManifest.bundleIdentifier}`,
`File size: ${candidateManifest.size}`,
`SHA-256: ${ipaSha256}`,
].join("\n"));
} finally {
await rm(temporaryDirectory, { recursive: true, force: true });
}
+251 -12
View File
@@ -4,11 +4,18 @@ import Ajv from "ajv";
import { execFile } from "node:child_process";
import { mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { extname, join, relative, resolve, sep } from "node:path";
import { basename, extname, join, relative, resolve, sep } from "node:path";
import { promisify } from "node:util";
import { parseOptions, setOptionFlag, setOptionValue } from "./cli.js";
import { bundleIdentifier, repositoryRoot, sourceIdentifier } from "./constants.js";
import {
bundleIdentifier,
canonicalBaseUrl,
canonicalSourceUrl,
repositoryRoot,
sourceIdentifier,
} from "./constants.js";
import { ipaFileManifest } from "./ipa-metadata.js";
const execFileAsync = promisify(execFile);
const schemaUrl = "https://raw.githubusercontent.com/SideStore/sidestore-source-types/main/schema.json";
@@ -17,6 +24,7 @@ const generatorPath = resolve(repositoryRoot, "scripts/generate-source.js");
const defaults = {
sourcePath: "apps.json",
checksumPath: "checksums.json",
ipaDirectory: "ipas",
offlineFallback: true,
legacyPurge: true,
};
@@ -41,6 +49,7 @@ const parseArguments = (cliArguments) => parseOptions(
{
"--source": setOptionValue("sourcePath"),
"--checksums": setOptionValue("checksumPath"),
"--ipa-dir": setOptionValue("ipaDirectory"),
"--skip-offline-fallback": setOptionFlag("offlineFallback", false),
"--skip-legacy-purge": setOptionFlag("legacyPurge", false),
},
@@ -52,6 +61,88 @@ const jsonDocument = async (jsonPath) => {
return JSON.parse(jsonText);
};
const fileBuffer = async (filePath) => readFile(filePath);
const relativePath = (entryPath) => relative(repositoryRoot, entryPath).split(sep).join("/");
const filesMatch = async (leftPath, rightPath) => {
try {
const [leftBuffer, rightBuffer] = await Promise.all([
fileBuffer(leftPath),
fileBuffer(rightPath),
]);
return leftBuffer.equals(rightBuffer);
} catch (filesystemError) {
if (filesystemError?.code === "ENOENT") {
return false;
}
throw filesystemError;
}
};
const assertFileExists = async (filePath, label, errors) => {
try {
const fileStats = await stat(filePath);
if (!fileStats.isFile()) {
errors.push(`${label} must be a file.`);
return null;
}
return fileStats;
} catch (filesystemError) {
if (filesystemError?.code === "ENOENT") {
errors.push(`${label} is missing.`);
return null;
}
throw filesystemError;
}
};
const canonicalOrigin = new URL(canonicalBaseUrl).origin;
const legacyHost = "j1coding.github.io";
const legacyPathPrefix = "/Armsx2-Repo";
const parsedUrl = (urlValue) => {
try {
return new URL(urlValue);
} catch {
return null;
}
};
const isCanonicalPublicUrl = (urlValue) => {
const url = parsedUrl(urlValue);
return Boolean(url && url.origin === canonicalOrigin);
};
const containsLegacySourceUrl = (payload) => {
const payloadText = JSON.stringify(payload);
return payloadText.includes(legacyHost) && payloadText.includes(legacyPathPrefix);
};
const urlPathToRepositoryPath = (urlValue) => {
const url = parsedUrl(urlValue);
if (!url || url.origin !== canonicalOrigin) {
return null;
}
const pathSegments = url.pathname
.split("/")
.filter(Boolean)
.map((pathSegment) => decodeURIComponent(pathSegment));
if (pathSegments.some((pathSegment) => pathSegment === "." || pathSegment === "..")) {
return null;
}
return resolve(repositoryRoot, ...pathSegments);
};
const sideStoreSchema = async () => {
const schemaResponse = await fetch(schemaUrl, {
headers: {
@@ -106,6 +197,14 @@ const validateStrictSourceShape = (sourceJson) => {
errors.push(`apps.json identifier must be ${sourceIdentifier}.`);
}
if (sourceJson.sourceURL !== canonicalSourceUrl) {
errors.push(`apps.json sourceURL must be ${canonicalSourceUrl}.`);
}
if (containsLegacySourceUrl(sourceJson)) {
errors.push("apps.json must not contain the old GitHub Pages source URL.");
}
const forbiddenSourceKeys = ["buildVersion", "sha256", "appPermissions", "marketplaceID"];
for (const forbiddenKey of forbiddenSourceKeys) {
@@ -121,20 +220,34 @@ const validateStrictSourceShape = (sourceJson) => {
errors.push(`apps[${appIndex}].bundleIdentifier must be ${bundleIdentifier}.`);
}
if (!Array.isArray(sourceApp.screenshotURLs) || sourceApp.screenshotURLs.length === 0) {
errors.push(`apps[${appIndex}].screenshotURLs must contain at least one screenshot URL.`);
continue;
if (!isCanonicalPublicUrl(sourceApp.iconURL)) {
errors.push(`apps[${appIndex}].iconURL must use ${canonicalBaseUrl}.`);
}
for (const [screenshotIndex, screenshotURL] of sourceApp.screenshotURLs.entries()) {
try {
const parsedScreenshotURL = new URL(screenshotURL);
if (!Array.isArray(sourceApp.screenshotURLs) || sourceApp.screenshotURLs.length === 0) {
errors.push(`apps[${appIndex}].screenshotURLs must contain at least one screenshot URL.`);
} else {
for (const [screenshotIndex, screenshotURL] of sourceApp.screenshotURLs.entries()) {
const parsedScreenshotURL = parsedUrl(screenshotURL);
if (!parsedScreenshotURL) {
errors.push(`apps[${appIndex}].screenshotURLs[${screenshotIndex}] must be an absolute URL.`);
continue;
}
if (parsedScreenshotURL.protocol !== "https:") {
errors.push(`apps[${appIndex}].screenshotURLs[${screenshotIndex}] must use HTTPS.`);
}
} catch {
errors.push(`apps[${appIndex}].screenshotURLs[${screenshotIndex}] must be an absolute URL.`);
if (parsedScreenshotURL.origin !== canonicalOrigin) {
errors.push(`apps[${appIndex}].screenshotURLs[${screenshotIndex}] must use ${canonicalBaseUrl}.`);
}
}
}
for (const [versionIndex, sourceVersion] of sourceApp.versions?.entries?.() ?? []) {
if (!isCanonicalPublicUrl(sourceVersion.downloadURL)) {
errors.push(`apps[${appIndex}].versions[${versionIndex}].downloadURL must use ${canonicalBaseUrl}.`);
}
}
}
@@ -153,6 +266,14 @@ const validateChecksumManifest = (sourceJson, checksumJson) => {
errors.push("checksums.json sourceURL must match apps.json sourceURL.");
}
if (checksumJson.sourceURL !== canonicalSourceUrl) {
errors.push(`checksums.json sourceURL must be ${canonicalSourceUrl}.`);
}
if (containsLegacySourceUrl(checksumJson)) {
errors.push("checksums.json must not contain the old GitHub Pages source URL.");
}
if (!Array.isArray(checksumJson.files)) {
errors.push("checksums.json files must be an array.");
return errors;
@@ -186,6 +307,121 @@ const validateChecksumManifest = (sourceJson, checksumJson) => {
if (!sourceDownloadURLs.has(checksumEntry.downloadURL)) {
errors.push(`${checksumPath}.downloadURL is absent from public/apps.json.`);
}
if (!isCanonicalPublicUrl(checksumEntry.downloadURL)) {
errors.push(`${checksumPath}.downloadURL must use ${canonicalBaseUrl}.`);
}
}
return errors;
};
const matchingSourceVersions = (sourceJson) =>
(sourceJson.apps ?? []).flatMap((sourceApp) =>
(sourceApp.versions ?? []).map((sourceVersion) => ({
app: sourceApp,
version: sourceVersion,
})),
);
const validateGeneratedMirrors = async (validationOptions) => {
const errors = [];
if (validationOptions.sourcePath === defaults.sourcePath) {
const rootSourcePath = resolve(repositoryRoot, validationOptions.sourcePath);
const publicSourcePath = resolve(repositoryRoot, "public/apps.json");
if (!await filesMatch(rootSourcePath, publicSourcePath)) {
errors.push("public/apps.json must match apps.json.");
}
}
if (validationOptions.checksumPath === defaults.checksumPath) {
const rootChecksumsPath = resolve(repositoryRoot, validationOptions.checksumPath);
const publicChecksumsPath = resolve(repositoryRoot, "public/checksums.json");
if (!await filesMatch(rootChecksumsPath, publicChecksumsPath)) {
errors.push("public/checksums.json must match checksums.json.");
}
}
return errors;
};
const validateLocalAssets = async (sourceJson) => {
const errors = [];
const assetUrls = (sourceJson.apps ?? []).flatMap((sourceApp) => [
sourceApp.iconURL,
...(sourceApp.screenshotURLs ?? []),
]).filter(Boolean);
for (const assetUrl of assetUrls) {
const assetPath = urlPathToRepositoryPath(assetUrl);
if (!assetPath) {
errors.push(`${assetUrl} does not map to a local asset path.`);
continue;
}
const assetLabel = relativePath(assetPath);
await assertFileExists(assetPath, assetLabel, errors);
const publicAssetPath = resolve(repositoryRoot, "public", assetLabel);
await assertFileExists(publicAssetPath, `public/${assetLabel}`, errors);
}
return errors;
};
const validateLocalIpas = async (sourceJson, checksumJson, validationOptions) => {
const errors = [];
const ipaDirectory = resolve(repositoryRoot, validationOptions.ipaDirectory);
const sourceVersionsByDownloadURL = new Map(
matchingSourceVersions(sourceJson).map(({ version }) => [version.downloadURL, version]),
);
for (const [fileIndex, checksumEntry] of (checksumJson.files ?? []).entries()) {
const checksumPath = `files[${fileIndex}]`;
const sourceVersion = sourceVersionsByDownloadURL.get(checksumEntry.downloadURL);
const ipaPath = resolve(ipaDirectory, basename(checksumEntry.fileName ?? ""));
const fileStats = await assertFileExists(ipaPath, `${checksumPath}.fileName local IPA`, errors);
if (!fileStats) {
continue;
}
let manifest;
try {
manifest = await ipaFileManifest(ipaPath, { baseUrl: canonicalBaseUrl });
} catch (metadataError) {
const message = metadataError instanceof Error ? metadataError.message : String(metadataError);
errors.push(`${checksumPath}.fileName could not be read as a valid IPA: ${message}`);
continue;
}
if (fileStats.size !== checksumEntry.size) {
errors.push(`${checksumPath}.size must match ${relativePath(ipaPath)} (${fileStats.size}).`);
}
if (manifest.sha256 !== checksumEntry.sha256) {
errors.push(`${checksumPath}.sha256 must match ${relativePath(ipaPath)}.`);
}
if (manifest.version !== checksumEntry.version) {
errors.push(`${checksumPath}.version must match the local IPA version ${manifest.version}.`);
}
if (manifest.buildVersion !== checksumEntry.buildVersion) {
errors.push(`${checksumPath}.buildVersion must match the local IPA build ${manifest.buildVersion}.`);
}
if (sourceVersion && sourceVersion.version !== manifest.version) {
errors.push(`${checksumPath}.version must match apps.json version ${sourceVersion.version}.`);
}
if (sourceVersion && sourceVersion.size !== fileStats.size) {
errors.push(`${checksumPath}.size must match apps.json size ${sourceVersion.size}.`);
}
}
return errors;
@@ -220,7 +456,7 @@ const legacyScanRoots = [
"public/checksums.json",
];
const relativeRepositoryPath = (entryPath) => relative(repositoryRoot, entryPath).split(sep).join("/");
const relativeRepositoryPath = relativePath;
const isTextFile = (entryPath, entryStats) =>
entryStats.size <= 1024 * 1024
@@ -349,6 +585,9 @@ const runValidation = async () => {
...await validateAgainstSideStoreSchema(sourceJson),
...validateStrictSourceShape(sourceJson),
...validateChecksumManifest(sourceJson, checksumJson),
...await validateGeneratedMirrors(validationOptions),
...await validateLocalAssets(sourceJson),
...await validateLocalIpas(sourceJson, checksumJson, validationOptions),
...(validationOptions.offlineFallback ? await validateOfflineFallback() : []),
...(validationOptions.legacyPurge ? await validateLegacyPurge() : []),
];
@@ -357,7 +596,7 @@ const runValidation = async () => {
throw new SourceValidationError(errors);
}
console.log("apps.json validates against the current SideStore schema.");
console.log("apps.json and checksums.json validate against source, asset, and IPA checks.");
};
try {