citra-qt-installer/server.ts

278 lines
8 KiB
TypeScript
Raw Normal View History

2022-10-07 10:30:57 +02:00
import * as xml from "https://deno.land/x/xml@2.0.4/mod.ts";
import { which } from "https://deno.land/x/which@0.2.1/mod.ts";
2022-11-27 09:34:14 +01:00
interface ITargetLicense {
License: { [index: string]: string };
}
interface ITargetSource {
Name: string;
DisplayName: string;
Description: string;
Repo: string;
ScriptName: string;
Default: string;
Licenses: ITargetLicense[];
}
interface IOutputTarget {
Name: string;
DisplayName: string;
Version: string;
DownloadableArchives: string;
UpdateFile: {
"@UncompressedSize": number;
"@CompressedSize": number;
"@OS": string;
};
ReleaseDate: string;
Description: string;
Default: string;
Licenses: ITargetLicense[];
Script: string;
SHA: string;
}
2022-10-07 10:30:57 +02:00
const tempDir = "./temp";
const distDir = "/citra/nginx/citra_repo";
2022-11-27 09:34:14 +01:00
async function getReleases(repo: string) {
2022-10-07 10:30:57 +02:00
const result = await fetch(`https://api.github.com/repos/${repo}/releases`, {
2017-10-11 04:36:47 +02:00
headers: {
2022-10-07 10:30:57 +02:00
Accept: "application/vnd.github.v3+json",
"User-Agent": "Citra Installer - Repo (j-selby)",
},
2017-10-11 04:36:47 +02:00
});
2022-10-07 10:30:57 +02:00
return result.json();
}
2022-11-27 09:34:14 +01:00
async function checkExists(directory: string) {
2022-10-07 10:30:57 +02:00
try {
await Deno.stat(directory);
return true;
} catch (_) {
return false;
}
2017-08-20 04:58:36 +02:00
}
2022-10-07 10:30:57 +02:00
async function check7z() {
for (const bin of ["7zz", "7za"]) {
const path = await which(bin);
if (path) return path;
}
throw new Error("7-zip is not available!");
}
2022-11-27 09:34:14 +01:00
function bufferToHex(buffer: ArrayBuffer) {
2022-10-07 10:30:57 +02:00
return [...new Uint8Array(buffer)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
2022-11-27 09:34:14 +01:00
function getTopResultFor(jsonData: any, platform: string) {
2022-10-07 10:30:57 +02:00
for (const releaseKey in jsonData) {
2017-10-11 04:36:47 +02:00
const release = jsonData[releaseKey];
2022-10-07 10:30:57 +02:00
for (const assetKey in release.assets) {
2017-10-11 04:36:47 +02:00
const asset = release.assets[assetKey];
2022-10-07 10:30:57 +02:00
if (asset.name.indexOf(platform) !== -1 && asset.name.endsWith(".7z")) {
2017-10-11 04:36:47 +02:00
return {
2022-10-07 10:30:57 +02:00
release_id: release.tag_name.split("-")[1],
published_at: release.published_at.substr(0, 10),
name: asset.name,
size: asset.size,
hash: release.tag_name,
2017-10-11 04:36:47 +02:00
};
}
2017-08-20 04:58:36 +02:00
}
2017-10-11 04:36:47 +02:00
}
2017-08-20 04:58:36 +02:00
2022-10-07 10:30:57 +02:00
return { notFound: true };
2017-08-20 04:58:36 +02:00
}
// The Qt Installer Framework is a pain to build or download.
// Because all we need are a few 7-zipped + xml files, we might as well generate them for ourselves.
2022-11-27 09:34:14 +01:00
const targets: ITargetSource[] = [
2017-10-11 04:36:47 +02:00
{
2022-10-07 10:30:57 +02:00
Name: "org.citra.nightly.%platform%",
DisplayName: "Citra Nightly",
Description:
"The nightly builds of Citra are official, tested versions of Citra that are known to work.\n" +
"(%platform%, commit: %commithash%, release date: %releasedate%)",
Repo: "citra-emu/citra-nightly",
ScriptName: "nightly",
Default: "script",
Licenses: [
{
License: {
"@file": "license.txt",
"@name": "GNU General Public License v2.0",
},
},
],
2017-10-11 04:36:47 +02:00
},
{
2022-10-07 10:30:57 +02:00
Name: "org.citra.canary.%platform%",
DisplayName: "Citra Canary",
Description:
"An in-development version of Citra that uses changes that are relatively untested.\n" +
"(%platform%, commit: %commithash%, release date: %releasedate%)",
Repo: "citra-emu/citra-canary",
ScriptName: "canary",
Default: "script",
Licenses: [
{
License: {
"@file": "license.txt",
"@name": "GNU General Public License v2.0",
},
},
],
},
2017-08-20 04:58:36 +02:00
];
2022-10-07 10:30:57 +02:00
async function execute() {
2022-10-07 10:56:29 +02:00
const zipBin = await check7z();
2017-10-11 04:36:47 +02:00
// Clean up any temporary directories.
2022-10-07 10:30:57 +02:00
try {
await Deno.remove(tempDir, { recursive: true });
} catch (_) {
// nothing
}
2017-10-11 04:36:47 +02:00
// Get Git information
2022-10-07 10:30:57 +02:00
console.debug("Getting release info...");
for (const target of targets) {
2017-10-11 04:36:47 +02:00
target.Repo = await getReleases(target.Repo);
}
2022-10-07 10:30:57 +02:00
console.debug("Building metadata...");
2017-10-11 04:36:47 +02:00
// If updates available is still false at the end of the foreach loop
// then that means no releases have been made -- nothing to do.
2022-10-07 10:30:57 +02:00
let updatesAvailable = false;
2017-10-11 04:36:47 +02:00
// Updates.xml
2022-10-07 10:30:57 +02:00
const updates = {
ApplicationName: "{AnyApplication}",
ApplicationVersion: "1.0.0", // Separate from nightly / canary versions
Checksum: false, // As they are pulled straight from Github
2022-11-27 09:34:14 +01:00
PackageUpdate: new Array<IOutputTarget>(),
2022-10-07 10:30:57 +02:00
};
2022-11-27 09:34:14 +01:00
async function generate(targetSource: ITargetSource, platform: string) {
2022-10-07 10:30:57 +02:00
// Get Git metadata
const releaseData = getTopResultFor(targetSource.Repo, platform);
2022-11-27 09:34:14 +01:00
const name: string = targetSource.Name.replace("%platform%", platform);
2022-10-07 10:30:57 +02:00
if (releaseData.notFound === true) {
console.error(`Release information not found for ${name}!`);
return;
}
2017-10-11 04:36:47 +02:00
2022-10-07 10:30:57 +02:00
const scriptName = platform + "-" + targetSource.ScriptName;
const version = releaseData.release_id;
const targetMetadataFilePath = `${distDir}/${name}/${version}meta.7z`;
if (await checkExists(targetMetadataFilePath)) {
console.debug(
2022-11-27 09:34:14 +01:00
`Metadata information already exists for ${name} ${version}, skipping.`
2022-10-07 10:30:57 +02:00
);
} else {
console.info(`Building release information for ${name} ${version}.`);
updatesAvailable = true;
// Create the temporary working directory.
const workingDirectoryPath = `${tempDir}/${name}`;
await Deno.mkdir(workingDirectoryPath, { recursive: true });
// Copy license
await Deno.copyFile("license.txt", `${workingDirectoryPath}/license.txt`);
await Deno.copyFile(
`scripts/${scriptName}.qs`,
2022-11-27 09:34:14 +01:00
`${workingDirectoryPath}/installscript.qs`
2022-10-07 10:30:57 +02:00
);
// Create 7zip archive
const fileName = `${name}.meta.7z`;
const proc = Deno.run({
cmd: [zipBin, "a", fileName, name],
cwd: tempDir,
});
const status = (await proc.status()).code;
if (status !== 0) {
throw new Error(
2022-11-27 09:34:14 +01:00
`Error when creating ${name} archive. Exited with ${status}.`
2022-10-07 10:30:57 +02:00
);
2017-10-11 04:36:47 +02:00
}
2022-10-07 10:30:57 +02:00
// Copy the metadata file into the target path.
console.debug(
2022-11-27 09:34:14 +01:00
`Creating target metadata for ${name} at ${targetMetadataFilePath}`
2022-10-07 10:30:57 +02:00
);
await Deno.mkdir(`${distDir}/${name}`, { recursive: true });
await Deno.rename(`${tempDir}/${fileName}`, `${targetMetadataFilePath}`);
2017-10-11 04:36:47 +02:00
2022-10-07 10:30:57 +02:00
// Cleanup temporary working directory.
await Deno.remove(workingDirectoryPath, { recursive: true });
}
2017-10-11 04:36:47 +02:00
2022-10-07 10:30:57 +02:00
// Create metadata for the Update.xml
const metaHash = await crypto.subtle.digest(
"SHA-1",
2022-11-27 09:34:14 +01:00
await Deno.readFile(targetMetadataFilePath)
2022-10-07 10:30:57 +02:00
);
const target = {
Name: name,
DisplayName: targetSource.DisplayName.replace("%platform%", platform),
Version: version,
DownloadableArchives: releaseData.name,
2017-10-11 04:36:47 +02:00
// Because we cannot compute the uncompressed size ourselves, just give a generous estimate
// (to make sure they have enough disk space).
// OS flag is useless - i.e the installer stubs it :P
2022-10-07 10:30:57 +02:00
UpdateFile: {
"@UncompressedSize": Math.ceil(releaseData.size * 4.85),
2022-10-07 10:30:57 +02:00
"@CompressedSize": releaseData.size,
"@OS": "Any",
},
ReleaseDate: releaseData.published_at,
Description: targetSource.Description.replace("%platform%", platform)
.replace("%commithash%", releaseData.hash)
.replace("%releasedate%", releaseData.published_at),
Default: targetSource.Default,
Licenses: targetSource.Licenses,
Script: "installscript.qs",
SHA: bufferToHex(metaHash),
};
updates.PackageUpdate.push(target);
}
// 6/19/18 (Flame Sage) - MSVC builds have been disabled, removed it from the below array 'msvc'
await Promise.all(
["mingw", "osx", "linux"].map((platform) => {
return Promise.all(
2022-11-27 09:34:14 +01:00
targets.map((targetSource) => generate(targetSource, platform))
2022-10-07 10:30:57 +02:00
);
2022-11-27 09:34:14 +01:00
})
2022-10-07 10:30:57 +02:00
);
2017-08-20 04:58:36 +02:00
2017-10-11 04:36:47 +02:00
if (updatesAvailable) {
2022-10-07 10:30:57 +02:00
const updatesXml = xml.stringify({ Updates: updates }, { indentSize: 2 });
2017-10-11 04:36:47 +02:00
// Save Updates.xml
2022-10-07 10:30:57 +02:00
await Deno.writeTextFile(`${distDir}/Updates.xml`, updatesXml);
console.info("Wrote a new Updates.xml file -- updates available.");
2017-10-11 04:36:47 +02:00
} else {
2022-10-07 10:30:57 +02:00
console.info(
2022-11-27 09:34:14 +01:00
"No Citra binary release updates are available for the Updates.xml -- nothing to do."
2022-10-07 10:30:57 +02:00
);
2017-10-11 04:36:47 +02:00
}
2022-10-07 10:56:29 +02:00
await Deno.remove(tempDir, { recursive: true });
2017-08-20 04:58:36 +02:00
}
execute().catch((err) => {
2022-10-07 10:30:57 +02:00
console.error(err);
2017-08-20 05:42:53 +02:00
});