citra-qt-installer/server.ts
2022-11-27 01:34:14 -07:00

277 lines
8 KiB
TypeScript

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";
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;
}
const tempDir = "./temp";
const distDir = "/citra/nginx/citra_repo";
async function getReleases(repo: string) {
const result = await fetch(`https://api.github.com/repos/${repo}/releases`, {
headers: {
Accept: "application/vnd.github.v3+json",
"User-Agent": "Citra Installer - Repo (j-selby)",
},
});
return result.json();
}
async function checkExists(directory: string) {
try {
await Deno.stat(directory);
return true;
} catch (_) {
return false;
}
}
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!");
}
function bufferToHex(buffer: ArrayBuffer) {
return [...new Uint8Array(buffer)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
function getTopResultFor(jsonData: any, platform: string) {
for (const releaseKey in jsonData) {
const release = jsonData[releaseKey];
for (const assetKey in release.assets) {
const asset = release.assets[assetKey];
if (asset.name.indexOf(platform) !== -1 && asset.name.endsWith(".7z")) {
return {
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,
};
}
}
}
return { notFound: true };
}
// 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.
const targets: ITargetSource[] = [
{
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",
},
},
],
},
{
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",
},
},
],
},
];
async function execute() {
const zipBin = await check7z();
// Clean up any temporary directories.
try {
await Deno.remove(tempDir, { recursive: true });
} catch (_) {
// nothing
}
// Get Git information
console.debug("Getting release info...");
for (const target of targets) {
target.Repo = await getReleases(target.Repo);
}
console.debug("Building metadata...");
// If updates available is still false at the end of the foreach loop
// then that means no releases have been made -- nothing to do.
let updatesAvailable = false;
// Updates.xml
const updates = {
ApplicationName: "{AnyApplication}",
ApplicationVersion: "1.0.0", // Separate from nightly / canary versions
Checksum: false, // As they are pulled straight from Github
PackageUpdate: new Array<IOutputTarget>(),
};
async function generate(targetSource: ITargetSource, platform: string) {
// Get Git metadata
const releaseData = getTopResultFor(targetSource.Repo, platform);
const name: string = targetSource.Name.replace("%platform%", platform);
if (releaseData.notFound === true) {
console.error(`Release information not found for ${name}!`);
return;
}
const scriptName = platform + "-" + targetSource.ScriptName;
const version = releaseData.release_id;
const targetMetadataFilePath = `${distDir}/${name}/${version}meta.7z`;
if (await checkExists(targetMetadataFilePath)) {
console.debug(
`Metadata information already exists for ${name} ${version}, skipping.`
);
} 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`,
`${workingDirectoryPath}/installscript.qs`
);
// 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(
`Error when creating ${name} archive. Exited with ${status}.`
);
}
// Copy the metadata file into the target path.
console.debug(
`Creating target metadata for ${name} at ${targetMetadataFilePath}`
);
await Deno.mkdir(`${distDir}/${name}`, { recursive: true });
await Deno.rename(`${tempDir}/${fileName}`, `${targetMetadataFilePath}`);
// Cleanup temporary working directory.
await Deno.remove(workingDirectoryPath, { recursive: true });
}
// Create metadata for the Update.xml
const metaHash = await crypto.subtle.digest(
"SHA-1",
await Deno.readFile(targetMetadataFilePath)
);
const target = {
Name: name,
DisplayName: targetSource.DisplayName.replace("%platform%", platform),
Version: version,
DownloadableArchives: releaseData.name,
// 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
UpdateFile: {
"@UncompressedSize": Math.ceil(releaseData.size * 4.85),
"@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(
targets.map((targetSource) => generate(targetSource, platform))
);
})
);
if (updatesAvailable) {
const updatesXml = xml.stringify({ Updates: updates }, { indentSize: 2 });
// Save Updates.xml
await Deno.writeTextFile(`${distDir}/Updates.xml`, updatesXml);
console.info("Wrote a new Updates.xml file -- updates available.");
} else {
console.info(
"No Citra binary release updates are available for the Updates.xml -- nothing to do."
);
}
await Deno.remove(tempDir, { recursive: true });
}
execute().catch((err) => {
console.error(err);
});