| // Yarn plugin to download binary artifact releases from GitHub and make |
| // them available on the Yarn CLI. |
| // |
| // The plugin implements a Resolver and a Locator that take in dependency |
| // with reference specification like: |
| // |
| // github-release:org/repo:version/binary |
| // |
| // e.g. in package.json: |
| // |
| // { |
| // dependencies: { |
| // 'example-binary': 'github-release:example/binary:1.0/exe' |
| // } |
| // } |
| // |
| // With that example the `exe` binary can be invoked via `yarn run exe` |
| // (or shorter `yarn exe`. |
| // |
| // The plugin does this by creating a faux package containing the wrapper |
| // JavaScript file and the binary. So the resulting package can be cached |
| // in Yarn cache. |
| // |
| // There is limited support for template literals, so when needed the |
| // reference specification can contain an expression within `${...}`. |
| // |
| // For example it is possible to specify this dependency: |
| // |
| // { |
| // dependencies: { |
| // 'example-binary': 'github-release:example/binary:1.0/exe-${process.arch}' |
| // } |
| // } |
| // |
| // Even though that is possible, it might not be desired in some cases. |
| // Notably when the expression is platform dependent (such as in the example |
| // above) the package will be different (e.g. containing binaries for |
| // different platforms) and the hash of the package will be different from |
| // the value persisted in Yarn lockfile, and would cause an error. |
| // |
| const YARN_FS_VERSION = '3.0.1'; |
| |
| const reference_pattern = /^github-release:(?<organization>[^\/]+)\/(?<repository>[^:]+)(?::(?<version>[^\/]+))?\/(?<binary>[^:]+)$/g; |
| |
| const supports = (ref) => { |
| reference_pattern.lastIndex = 0; |
| return reference_pattern.test(ref); |
| } |
| |
| const parse = (ref) => { |
| const reference = new Function(`return \`${ref}\`;`).call(); |
| |
| reference_pattern.lastIndex = 0; |
| const parts = [...reference.matchAll(reference_pattern)][0]; |
| |
| return { |
| organization: parts[1], |
| repository: parts[2], |
| version: parts[3] || 'latest', |
| binary: parts[4] |
| } |
| } |
| |
| module.exports = { |
| name: `github-release-binary`, |
| factory: require => { |
| const util = require('util'); |
| const { httpUtils, structUtils, LinkType } = require('@yarnpkg/core'); |
| const { ppath, xfs } = require('@yarnpkg/fslib'); |
| const { ZipFS, getLibzipPromise } = require('@yarnpkg/libzip'); |
| |
| class GitHubReleaseFetcher { |
| |
| supports(locator, opts) { |
| return supports(locator.reference); |
| } |
| |
| getLocalPath(locator, opts) { |
| return null; |
| } |
| |
| async fetch(locator, opts) { |
| const expectedChecksum = opts.checksums.get(locator.locatorHash) || null; |
| |
| const [packageFs, releaseFs, checksum] = await opts.cache.fetchPackageFromCache(locator, expectedChecksum, { |
| onHit: () => opts.report.reportCacheHit(locator), |
| onMiss: () => opts.report.reportCacheMiss(locator, `${structUtils.prettyLocator(opts.project.configuration, locator)} can't be found in the cache and will be fetched from the remote server`), |
| loader: () => this.fetchFromNetwork(locator, opts), |
| skipIntegrityCheck: opts.skipIntegrityCheck, |
| }); |
| |
| return { |
| packageFs, |
| releaseFs, |
| prefixPath: structUtils.getIdentVendorPath(locator), |
| checksum, |
| }; |
| } |
| |
| async fetchFromNetwork(locator, opts) { |
| // 1980-01-01, like Fedora |
| const defaultTime = 315532800; |
| |
| const parts = parse(locator.reference); |
| |
| const releaseBuffer = await httpUtils.get(`https://github.com/${parts.organization}/${parts.repository}/releases/download/${parts.version}/${parts.binary}`, { |
| configuration: opts.project.configuration, |
| }); |
| |
| const packageName = ppath.join(locator.scope !== null ? '@' + locator.scope : '', locator.name); |
| |
| const tmpDir = xfs.mktempSync(); |
| |
| const zipFS = new ZipFS(ppath.join(tmpDir, 'release.zip'), { create: true, libzip: await getLibzipPromise() }); |
| |
| zipFS.writeFileSync('package.json', `{ "name": "${packageName}", "dependencies": { "@yarnpkg/fslib": "${YARN_FS_VERSION}" } }`); |
| zipFS.utimesSync('package.json', defaultTime, defaultTime); |
| |
| const dir = ppath.join('node_modules', packageName); |
| zipFS.mkdirSync(dir, { recursive: true }); |
| |
| const stubFile = ppath.join(dir, 'exec.js'); |
| zipFS.writeFileSync(stubFile, `const { xfs } = require('@yarnpkg/fslib'); |
| const fs = require('fs'); |
| const path = require('path'); |
| const os = require('os'); |
| const { spawn } = require('child_process'); |
| |
| const execute = (path, args) => { |
| const child = spawn(path, args); |
| |
| process.stdin.pipe(child.stdin); |
| child.stdout.pipe(process.stdout); |
| child.stderr.pipe(process.stderr); |
| |
| child.on('error', err => { process.stderr.write(err + '\\n'); process.exit(1) } ); |
| child.on('exit', status => process.exit(status)); |
| } |
| |
| (async () => { |
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'github-release-binary')); |
| process.on('exit', () => { |
| fs.rmdirSync(tmpDir, { recursive: true }); |
| }) |
| |
| const binary = process.argv[1].replace('exec.js', '${parts.binary}'); |
| const binaryPath = path.join(tmpDir, '${parts.binary}'); |
| await xfs.copyFilePromise(binary, binaryPath); |
| await xfs.chmodPromise(binaryPath, 0o755); |
| execute(binaryPath, process.argv.slice(2)); |
| })();`); |
| zipFS.chmodSync(stubFile, 0o755); |
| zipFS.utimesSync(stubFile, defaultTime, defaultTime); |
| |
| const binaryPath = ppath.join(dir, parts.binary); |
| zipFS.writeFileSync(binaryPath, releaseBuffer); |
| zipFS.chmodSync(binaryPath, 0o755); |
| zipFS.utimesSync(binaryPath, defaultTime, defaultTime); |
| |
| xfs.detachTemp(tmpDir); |
| |
| return zipFS; |
| } |
| |
| } |
| |
| class GitHubReleaseResolver { |
| |
| supportsDescriptor(descriptor, opts) { |
| return supports(descriptor.range); |
| } |
| |
| supportsLocator(locator, opts) { |
| return supports(locator.reference); |
| } |
| |
| shouldPersistResolution(locator, opts) { |
| return true; |
| } |
| |
| bindDescriptor(descriptor, fromLocator, opts) { |
| return descriptor; |
| } |
| |
| getResolutionDependencies(descriptor, opts) { |
| return []; |
| } |
| |
| async getCandidates(descriptor, dependencies, opts) { |
| return [structUtils.convertDescriptorToLocator(descriptor)]; |
| } |
| |
| async getSatisfying(descriptor, dependencies, locators, opts) { |
| return { |
| locators |
| }; |
| } |
| |
| async resolve(locator, opts) { |
| const parts = parse(locator.reference); |
| |
| const fsLibDep = structUtils.makeDescriptor(structUtils.makeIdent('yarnpkg', 'fslib'), YARN_FS_VERSION) |
| |
| const dependencies = new Map(); |
| dependencies.set(fsLibDep.identHash, fsLibDep); |
| |
| return { |
| ...locator, |
| version: parts.version, |
| languageName: opts.project.configuration.get(`defaultLanguageName`), |
| linkType: LinkType.HARD, |
| dependencies: opts.project.configuration.normalizeDependencyMap(dependencies), |
| bin: [ |
| [ parts.binary, 'exec.js' ] |
| ] |
| }; |
| } |
| |
| } |
| |
| return { |
| fetchers: [ GitHubReleaseFetcher ], |
| resolvers: [ GitHubReleaseResolver ], |
| } |
| } |
| }; |