Skip to content

Commit

Permalink
feat: Allow mapping of architectures too
Browse files Browse the repository at this point in the history
  • Loading branch information
prantlf committed Dec 12, 2023
1 parent 5111952 commit 8d61855
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 67 deletions.
35 changes: 24 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,24 @@ import grab from 'grab-github-release'

try {
const repository = 'prantlf/v-jsonlint'
const platformSuffixes = {
darwin: 'macos',
win32: 'windows'
}
// downloads and unpacks the jsonlint executable to the current directory
await grab({ repository, platformSuffixes, unpackExecutable: true })
await grab({ repository, unpackExecutable: true })
} catch(err) {
console.error(err.message)
process.exitCode = 1
}
```

The archive with the executable is expected to be:

{name}-{platform}-{architecture}.zip

where:

* `{name}` is the name of the tool (executable)
* `{platform}` is the name of the target platform, by default: `linux`, `macos` or `windows`
* `{architecture}` is the name of the targetarchitecture, by default `aarch64` or `arm64` (64-bit ARM), `amd64`, `x86_64`, `x64` or `x86` (64-bit AMD)

## Installation

This package can be installed globally, if you want to use the `grab-github-release` script (or the `ggr` alias). You can install it during the first usage with `npx` too:
Expand All @@ -54,7 +60,8 @@ Make sure, that you use [Node.js] version 18 or newer.
-r|--repository <repository> GitHub repository formatted "owner/name"
-i|--version-spec <semver> semantic version specifier or "latest"
-n|--name <file-name> archive name without the platform suffix
-p|--platform-suffixes <map> unpack the executable and remove the archive
-p|--platform-suffixes <map> platform name mapping
-a|--arch-suffixes <map> architecture name mapping
-e|--unpack-exe unpack the executable and remove the archive
-v|--verbose prints extra information on the console
-V|--version print version number and exit
Expand All @@ -64,15 +71,15 @@ Make sure, that you use [Node.js] version 18 or newer.
from the first archive asset found for the current platform, if not specified.

Examples:
$ grab-github-release -r prantlf/v-jsonlint -p darwin=macos,win32=windows -u
$ grab-github-release -r prantlf/v-jsonlint -p darwin=macos,win32=windows:win64 -u
$ grab-github-release -r prantlf/v-jsonlint -i >=0.0.6

## API

```ts
// map where keys are Node.js platform names and values are their replacements
// to be used in names of archive looked for among GitHub release assets
type PlatformSuffixes = Record<string, string>
type ArchiveSuffixes = Record<string, string[]>

interface GrabOptions {
// GitHub repository formatted "owner/name", mandatory
Expand All @@ -82,9 +89,15 @@ interface GrabOptions {
// archive name without the platform and architecture suffix
// and without the ".zip" extension as well
name?: string
// archive name without the platform suffix; if not specified, it will be
// inferred from the first archive asset found for the current platform
platformSuffixes?: PlatformSuffixes
// recognised platforms organised by the Node.js platform name; defaults:
// - darwin: darwin, macos
// - linux: linux
// - win32: win32, windows
platformSuffixes?: ArchiveSuffixes
// recognised architectures organised by the Node.js platform name; defaults:
// - arm64: aarch64, arm64
// - x64: amd64, x86_64, x64, x86
archSuffixes?: ArchiveSuffixes
// directory to write the archive or executable to; if not specified,
// files will be written to the current directory
targetDirectory?: string
Expand Down
21 changes: 16 additions & 5 deletions bin/grab-github-release.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ Options:
-r|--repository <repository> GitHub repository formatted "owner/name"
-i|--version-spec <semver> semantic version specifier or "latest"
-n|--name <file-name> archive name without the platform suffix
-p|--platform-suffixes <map> unpack the executable and remove the archive
-p|--platform-suffixes <map> platform name mapping
-a|--arch-suffixes <map> architecture name mapping
-t|--target-dir <dir-name> directory to write the output files to
-e|--unpack-exe unpack the executable and remove the archive
-v|--verbose prints extra information on the console
Expand All @@ -22,7 +23,7 @@ The version specifier is "latest" by default. The file name will be inferred
from the first archive asset found for the current platform, if not specified.
Examples:
$ grab-github-release -r prantlf/v-jsonlint -p darwin=macos,win32=windows -u
$ grab-github-release -r prantlf/v-jsonlint -p darwin=macos,win32=windows:win64 -u
$ grab-github-release -r prantlf/v-jsonlint -i >=0.0.6`)
}

Expand All @@ -32,7 +33,8 @@ function fail(message) {
}

const { argv } = process
let repository, version, name, platformSuffixes, targetDirectory, unpackExecutable, verbose
let repository, version, name, platformSuffixes, archSuffixes,
targetDirectory, unpackExecutable, verbose

for (let i = 2, l = argv.length; i < l; ++i) {
const arg = argv[i]
Expand All @@ -56,7 +58,16 @@ for (let i = 2, l = argv.length; i < l; ++i) {
if (!platformSuffixes) platformSuffixes = {}
for (const entry of entries.trim().split(',')) {
const [key, val] = entry.trim().split('=')
platformSuffixes[key.trim()] = val.trim()
platformSuffixes[key.trim()] = val.trim().split(':').map(val => val.trim())
}
return
case 'a': case 'arch-suffixes':
entries = match[4] || argv[++i]
if (!entries) fail('missing architecture suffix map')
if (!archSuffixes) archSuffixes = {}
for (const entry of entries.trim().split(',')) {
const [key, val] = entry.trim().split('=')
archSuffixes[key.trim()] = val.trim().split(':').map(val => val.trim())
}
return
case 't': case 'target-dir':
Expand Down Expand Up @@ -102,7 +113,7 @@ if (!repository) {
}

try {
await grab({ repository, version, name, platformSuffixes, unpackExecutable, verbose })
await grab({ repository, version, name, platformSuffixes, archSuffixes, unpackExecutable, verbose })
} catch(err) {
console.error(err.message)
process.exitCode = 1
Expand Down
16 changes: 12 additions & 4 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* map where keys are Node.js platform names and values are their replacements
* to be used in names of archive looked for among GitHub release assets
*/
type PlatformSuffixes = Record<string, string>
type ArchiveSuffixes = Record<string, string[]>

interface GrabOptions {
/**
Expand All @@ -19,10 +19,18 @@ interface GrabOptions {
*/
name?: string
/**
* archive name without the platform suffix; if not specified, it will be
* inferred from the first archive asset found for the current platform
* recognised platforms organised by the Node.js platform name; defaults:
* - darwin: darwin, macos
* - linux: linux
* - win32: win32, windows
*/
platformSuffixes?: PlatformSuffixes
platformSuffixes?: ArchiveSuffixes
/**
* recognised architectures organised by the Node.js platform name; defaults:
* - arm64: aarch64, arm64
* - x64: amd64, x86_64, x64, x86
*/
archSuffixes?: ArchiveSuffixes
/**
* directory to write the archive or executable to; if not specified,
* files will be written to the current directory
Expand Down
61 changes: 36 additions & 25 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { open as openArchive } from 'yauzl'

let log = debug('grabghr')
const { chmod, unlink } = promises
const { platform, arch } = process
const { arch, platform } = process

/* c8 ignore next 5 */
function delay() {
Expand Down Expand Up @@ -43,15 +43,29 @@ function fetchSafely(url) {
})
}

function getArchiveSuffix(platformSuffixes) {
/* c8 ignore next */
const plat = platformSuffixes && platformSuffixes[platform] || platform
return `-${plat}-${arch}.zip`
const defaultPlatformSuffixes = {
darwin: ['macos'],
linux: [],
win32: ['windows']
}

async function getRelease(name, repo, verspec, platformSuffixes) {
const suffix = getArchiveSuffix(platformSuffixes)
const archive = name && `${name}${suffix}`
const defaultArchSuffixes = {
arm64: ['aarch64'],
x64: ['amd64', 'x86_64', 'x86']
}

function getArchiveSuffixes(platformSuffixes, archSuffixes) {
/* c8 ignore next 3 */
const plats = platformSuffixes && platformSuffixes[platform] || defaultPlatformSuffixes[platform] || []
if (!plats.includes(platform)) plats.push(platform)
const archs = archSuffixes && archSuffixes[arch] || defaultArchSuffixes[arch] || []
if (!archs.includes(arch)) archs.push(arch)
return plats.map(plat => archs.map(arch => `-${plat}-${arch}.zip`)).flat()
}

async function getRelease(name, repo, verspec, platformSuffixes, archSuffixes) {
const suffixes = getArchiveSuffixes(platformSuffixes, archSuffixes)
const archives = name && suffixes.map(suffix => `${name}${suffix}`)
const url = `https://api.github.com/repos/${repo}/releases`
const res = await fetchSafely(url)
const releases = await res.json()
Expand All @@ -62,20 +76,23 @@ async function getRelease(name, repo, verspec, platformSuffixes) {
if (valid(version) && (verspec === 'latest' || satisfies(version, verspec))) {
log('match "%s" (%s)', version, tag_name)
for (const { name: file, browser_download_url: url } of assets) {
if (archive) {
if (file === archive) {
if (archives) {
if (archives.includes(file)) {
log('match "%s"', file)
return { name, version, archive, url }
return { name, version, archive: file, url }
}
} else {
const suffix = suffixes.find(suffix => file.endsWith(suffix))
if (suffix) {
log('match by suffix "%s"', file)
const name = file.substring(0, file.length - suffix.length)
return { name, version, archive: file, url }
}
} else if (file.endsWith(suffix)) {
log('match by suffix "%s"', file)
const name = file.substring(0, file.length - suffix.length)
return { name, version, archive: file, url }
}
log('skip "%s"', file)
}
/* c8 ignore next 7 */
throw new Error(`archive ${archive ? '"' + archive + '"' : 'ending with ' + suffix} not found in ${version}`)
throw new Error(`no suitable archive found for ${version}`)
} else {
log('skip "%s" (%s)', version, tag_name)
}
Expand Down Expand Up @@ -133,18 +150,12 @@ async function makeExecutable(executable) {
}
}

export default async function grab({ name, repository, version, platformSuffixes, targetDirectory, unpackExecutable, verbose }) {
export default async function grab({ name, repository, version, platformSuffixes, archSuffixes, targetDirectory, unpackExecutable, verbose }) {
if (verbose) log = console.log.bind(console)
if (!version) version = 'latest'
const verspec = clean(version) || version
let archive, url
if (name && valid(verspec)) {
version = verspec
archive = `${name}${getArchiveSuffix(platformSuffixes)}`
url = `https://github.com/${repository}/releases/download/v${version}/${archive}`
} else {
({ name, version, archive, url } = await getRelease(name, repository, verspec, platformSuffixes))
}
let archive, url;
({ name, version, archive, url } = await getRelease(name, repository, verspec, platformSuffixes, archSuffixes))
if (targetDirectory) archive = join(targetDirectory, archive)
await download(url, archive)
if (unpackExecutable) {
Expand Down
37 changes: 20 additions & 17 deletions test/mocked.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,25 @@ import releases from './data/releases.json' assert { type: 'json' }

const exists = file => access(file).then(() => true, () => false)
const __dirname = dirname(fileURLToPath(import.meta.url))
const { platform, arch } = process
const { arch, platform } = process

const repository = 'prantlf/v-jsonlint'
const name = 'jsonlint'
const executable = join('.', platform != 'win32' ? name : `${name}.exe`)
const version = '0.0.6'
const platformSuffixes = {
linux: 'linux',
darwin: 'macos',
win32: 'windows'
linux: ['linux'],
darwin: ['macos'],
win32: ['windows']
}
const archive = `${name}-${platformSuffixes[platform]}-${arch}.zip`
const archSuffixes = {
arm64: ['arm64'],
x64: ['x64', 'x86_64']
}
const archive = `${name}-${platformSuffixes[platform][0]}-${archSuffixes[arch][0]}.zip`
const content = new Blob(
[await readFile(join(__dirname, `data/${archive}`))],
{ type: 'applicaiton.zip' }
{ type: 'application.zip' }
)
const targetDirectory = join(__dirname, 'tmp')

Expand Down Expand Up @@ -64,56 +68,55 @@ after(async () => {
})

test('download archive from the latest fixed version', async () => {
const { archive: actualArchive, version: actualVersion } = await grab(
{ name, repository, version, platformSuffixes })
const { archive: actualArchive, version: actualVersion } =
await grab({ name, repository, version })
ok(await exists(archive), 'archive not found')
strictEqual(actualVersion, version)
strictEqual(actualArchive, archive)
})

test('download archive from the latest symbolic version', async () => {
test('download archive from the latest symbolic version with platforms', async () => {
const { archive: actualArchive, version: actualVersion } = await grab(
{ name, repository, version: 'latest', platformSuffixes })
ok(await exists(archive), 'archive not found')
strictEqual(actualVersion, version)
strictEqual(actualArchive, archive)
})

test('download archive from the latest semantic version', async () => {
test('download archive from the latest semantic version with architectures', async () => {
const { archive: actualArchive, version: actualVersion } = await grab(
{ name, repository, version: '>=0.0.1', platformSuffixes })
{ name, repository, version: '>=0.0.1', archSuffixes })
ok(await exists(archive), 'archive not found')
strictEqual(actualVersion, version)
strictEqual(actualArchive, archive)
})

test('download archive from a fixed tag', async () => {
const { archive: actualArchive, version: actualVersion } = await grab(
{ name, repository, version: `v${version}`, platformSuffixes })
{ name, repository, version: `v${version}` })
ok(await exists(archive), 'archive not found')
strictEqual(actualVersion, version)
strictEqual(actualArchive, archive)
})

test('download archive from an old fixed version', async () => {
const { archive: actualArchive, version: actualVersion } = await grab(
{ name, repository, version: '0.0.5', platformSuffixes })
{ name, repository, version: '0.0.5' })
ok(await exists(archive), 'archive not found')
strictEqual(actualVersion, '0.0.5')
strictEqual(actualArchive, archive)
})

test('download archive from the latest fixed version with a guessed name', async () => {
const { archive: actualArchive, version: actualVersion } = await grab(
{ repository, version, platformSuffixes })
const { archive: actualArchive, version: actualVersion } = await grab({ repository, version })
ok(await exists(archive), 'archive not found')
strictEqual(actualVersion, version)
strictEqual(actualArchive, archive)
})

test('download archive from the latest implicit version and unpack executable', async () => {
const { executable: actualExecutable, version: actualVersion } = await grab(
{ name, repository, platformSuffixes, unpackExecutable: true, verbose: true })
{ name, repository, unpackExecutable: true, verbose: true })
ok(!await exists(archive), 'archive found')
ok(await exists(executable), 'executable not found')
strictEqual(actualVersion, version)
Expand All @@ -123,7 +126,7 @@ test('download archive from the latest implicit version and unpack executable',
test('download archive from the latest implicit version and unpack executable to a custom directory', async () => {
await mkdir(targetDirectory, { recursive: true })
const { executable: actualExecutable, version: actualVersion } = await grab(
{ name, repository, platformSuffixes, targetDirectory, unpackExecutable: true })
{ name, repository, targetDirectory, unpackExecutable: true })
ok(await exists(actualExecutable), 'executable not found')
strictEqual(actualVersion, version)
ok(actualExecutable.endsWith(executable))
Expand Down
4 changes: 2 additions & 2 deletions test/real.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ before(cleanup)
after(cleanup)

test('download archive from a fixed version', async () => {
const { archive: actualArchive, version: actualVersion } = await grab(
{ name, repository, version, platformSuffixes })
const { archive: actualArchive, version: actualVersion } =
await grab({ name, repository, version })
if (!await exists(archive)) throw new Error('archive not found')
strictEqual(actualVersion, version)
strictEqual(actualArchive, archive)
Expand Down
10 changes: 7 additions & 3 deletions test/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ test('Type declarations for TypeScript', () => {
name: '',
version: '',
platformSuffixes: {
linux: '',
darwin: '',
win32: ''
linux: [''],
darwin: [''],
win32: ['']
},
archSuffixes: {
arm64: [''],
x64: ['']
},
targetDirectory: '',
unpackExecutable: true
Expand Down

0 comments on commit 8d61855

Please sign in to comment.