const xml = require('xml'); const fs = require('fs-extra'); const exec = require('execa'); const sha1 = require('sha1-file'); const req = require('request-promise'); const zipBin = require('7zip-bin').path7za; const logger = require('winston'); logger.exitOnError = false; logger.add(logger.transports.File, { filename: '/var/log/citra-qt-installer/citra-qt-installer-repository.log' }); const tempDir = './temp'; const distDir = '/citra/nginx/repo'; async function getReleases (repo) { const result = await req({ uri: `https://api.github.com/repos/${repo}/releases`, headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'Citra Installer - Repo (j-selby)' } }); return JSON.parse(result); } function getTopResultFor (jsonData, platform) { for (let releaseKey in jsonData) { const release = jsonData[releaseKey]; for (let 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. let targets = [ { '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': [{_attr: { 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': [{_attr: { file: 'license.txt', name: 'GNU General Public License v2.0' }}]} ] } ]; async function execute () { // Clean up any temporary directories. fs.emptyDirSync(tempDir); // Get Git information logger.debug('Getting release info...'); for (var resultKey in targets) { const target = targets[resultKey]; target.Repo = await getReleases(target.Repo); } logger.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. var updatesAvailable = false; // Updates.xml let updates = [ {'ApplicationName': '{AnyApplication}'}, {'ApplicationVersion': '1.0.0'}, // Separate from nightly / canary versions {'Checksum': false} // As they are pulled straight from Github ]; ['msvc', 'mingw', 'osx', 'linux'].forEach((platform) => { targets.forEach((targetSource) => { // Get Git metadata const releaseData = getTopResultFor(targetSource.Repo, platform); const name = targetSource.Name.replace('%platform%', platform); if (releaseData.notFound === true) { logger.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 (fs.existsSync(targetMetadataFilePath)) { logger.debug(`Metadata information already exists for ${name} ${version}, skipping.`); } else { logger.info(`Building release information for ${name} ${version}.`); updatesAvailable = true; // Create the temporary working directory. const workingDirectoryPath = `${tempDir}/${name}`; fs.ensureDirSync(workingDirectoryPath); // Copy license fs.copySync('license.txt', `${workingDirectoryPath}/license.txt`); fs.copySync(`scripts/${scriptName}.qs`, `${workingDirectoryPath}/installscript.qs`); // Create 7zip archive exec.sync(zipBin, ['a', 'meta.7z', name], {'cwd': tempDir}); // Copy the metadata file into the target path. logger.debug(`Creating target metadata for ${name} at ${targetMetadataFilePath}`); fs.moveSync(`${tempDir}/meta.7z`, targetMetadataFilePath); // Cleanup temporary working directory. fs.removeSync(workingDirectoryPath); } // Create metadata for the Update.xml var metaHash = sha1(targetMetadataFilePath); let target = []; target.push({'Name': name}); target.push({'DisplayName': targetSource.DisplayName.replace('%platform%', platform)}); target.push({'Version': version}); target.push({'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 target.push({'UpdateFile': [{_attr: { UncompressedSize: releaseData.size * 2, CompressedSize: releaseData.size, OS: 'Any'} }]}); target.push({'ReleaseDate': releaseData.published_at}); target.push({'Description': targetSource.Description .replace('%platform%', platform) .replace('%commithash%', releaseData.hash) .replace('%releasedate%', releaseData.published_at)}); target.push({'Default': targetSource.Default}); target.push({'Licenses': targetSource.Licenses}); target.push({'Script': 'installscript.qs'}); target.push({'SHA': metaHash}); updates.push({'PackageUpdate': target}); }); }); if (updatesAvailable) { const updatesXml = xml({'Updates': updates}, {indent: ' '}); // Save Updates.xml fs.writeFileSync(`${distDir}/Updates.xml`, updatesXml); logger.info('Wrote a new Updates.xml file -- updates are available.'); } else { logger.info('No Citra binary release updates are available for the Updates.xml -- nothing to do.'); } } execute().catch((err) => { logger.error(err); });