-
Notifications
You must be signed in to change notification settings - Fork 51
feat(targets): add pkg-pacman target (Arch Linux AUR / PKGBUILD) #486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d92196f
50070df
36bfaaf
ca6da2b
d687337
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # Arch Linux AUR / Pacman | ||
|
|
||
| Provides the `pkg-pacman` sh1pt target adapter for generating `PKGBUILD` and `.SRCINFO` | ||
| files and publishing packages to the [Arch User Repository (AUR)](https://aur.archlinux.org). | ||
|
|
||
| ## What it does | ||
|
|
||
| - Generates a valid `PKGBUILD` file for Arch Linux / Pacman | ||
| - Generates the matching `.SRCINFO` metadata file (required by AUR) | ||
| - Publishes to AUR via SSH on `sh1pt promote ship` | ||
| - Supports x86_64, aarch64, and noarch architectures | ||
|
|
||
| ## Package | ||
|
|
||
| - Name: `@profullstack/sh1pt-target-pkg-pacman` | ||
| - Path: `packages/targets/pkg-pacman` | ||
| - Adapter ID: `pkg-pacman` | ||
| - Homepage: https://sh1pt.com | ||
|
|
||
| ## Setup | ||
|
|
||
| ```bash | ||
| sh1pt secret set AUR_SSH_KEY <path-to-your-aur-ssh-private-key> | ||
| ``` | ||
|
|
||
| 1. Register at [aur.archlinux.org](https://aur.archlinux.org) | ||
| 2. Add your SSH public key in AUR account settings | ||
| 3. Clone your package: `git clone ssh://aur@aur.archlinux.org/<pkgname>.git` | ||
|
|
||
| See the [AUR submission guidelines](https://wiki.archlinux.org/title/AUR_submission_guidelines) for details. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "name": "@profullstack/sh1pt-target-pkg-pacman", | ||
| "version": "0.1.0", | ||
| "type": "module", | ||
| "main": "./src/index.ts", | ||
| "scripts": { | ||
| "build": "tsc -p tsconfig.json", | ||
| "typecheck": "tsc -p tsconfig.json --noEmit", | ||
| "prepublishOnly": "pnpm build" | ||
| }, | ||
| "dependencies": { | ||
| "@profullstack/sh1pt-core": "workspace:*" | ||
| }, | ||
| "license": "MIT", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/profullstack/sh1pt.git", | ||
| "directory": "packages/targets/pkg-pacman" | ||
| }, | ||
| "homepage": "https://sh1pt.com", | ||
| "bugs": "https://github.com/profullstack/sh1pt/issues", | ||
| "files": ["dist"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import { fakeBuildContext, fakeShipContext, smokeTest } from '@profullstack/sh1pt-core/testing'; | ||
| import { readFile, rm, mkdtemp } from 'node:fs/promises'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
| import { afterEach, describe, expect, it } from 'vitest'; | ||
| import adapter from './index.js'; | ||
|
|
||
| smokeTest(adapter, { idPrefix: 'pkg', requireKind: true }); | ||
|
|
||
| const tempDirs: string[] = []; | ||
| afterEach(async () => { | ||
| await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); | ||
| }); | ||
|
|
||
| describe('PKGBUILD generation', () => { | ||
| it('writes PKGBUILD and .SRCINFO from config', async () => { | ||
| const outDir = await mkdtemp(join(tmpdir(), 'sh1pt-pacman-')); | ||
| tempDirs.push(outDir); | ||
|
|
||
| const result = await adapter.build(fakeBuildContext({ outDir, version: 'v3.0.1' }) as any, { | ||
| pkgname: 'myapp', | ||
| pkgdesc: 'An example CLI tool', | ||
| license: 'MIT', | ||
| url: 'https://example.com', | ||
| arch: 'x86_64', | ||
| releaseRepo: 'acme/myapp', | ||
| depends: ['glibc', 'gcc-libs'], | ||
| sha512sum: 'a'.repeat(128), | ||
| }); | ||
|
|
||
| expect(result.artifact).toBe(join(outDir, 'PKGBUILD')); | ||
|
|
||
| const pkgbuild = await readFile(join(outDir, 'PKGBUILD'), 'utf-8'); | ||
| expect(pkgbuild).toContain('pkgname=myapp'); | ||
| expect(pkgbuild).toContain('pkgver=3.0.1'); | ||
| expect(pkgbuild).toContain('pkgdesc="An example CLI tool"'); | ||
| expect(pkgbuild).toContain("arch=('x86_64')"); | ||
| expect(pkgbuild).toContain("license=('MIT')"); | ||
| expect(pkgbuild).toContain("depends=('glibc' 'gcc-libs')"); | ||
| expect(pkgbuild).toContain('sha512sums=(\'' + 'a'.repeat(128) + '\')'); | ||
| expect(pkgbuild).toContain('package()'); | ||
|
|
||
| const srcinfo = await readFile(join(outDir, '.SRCINFO'), 'utf-8'); | ||
| expect(srcinfo).toContain('pkgbase = myapp'); | ||
| expect(srcinfo).toContain('pkgver = 3.0.1'); | ||
| }); | ||
|
|
||
| it('keeps dry-run shipping side-effect free', async () => { | ||
| await expect(adapter.ship(fakeShipContext({ version: '3.0.1', dryRun: true }) as any, { | ||
| pkgname: 'myapp', | ||
| })).resolves.toEqual({ id: 'dry-run' }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,148 @@ | ||||||
| import { defineTarget, manualSetup } from '@profullstack/sh1pt-core'; | ||||||
| import { mkdir, writeFile } from 'node:fs/promises'; | ||||||
| import { join } from 'node:path'; | ||||||
|
|
||||||
| interface Config { | ||||||
| /** Package name in the AUR / Pacman repo */ | ||||||
| pkgname: string; | ||||||
| /** Package description */ | ||||||
| pkgdesc?: string; | ||||||
| /** SPDX license identifier, e.g. "MIT" */ | ||||||
| license?: string; | ||||||
| /** Project homepage URL */ | ||||||
| url?: string; | ||||||
| /** Architecture: x86_64 | aarch64 | any */ | ||||||
| arch?: 'x86_64' | 'aarch64' | 'any'; | ||||||
| /** GitHub release repo to derive default download URL, e.g. "myorg/myapp" */ | ||||||
| releaseRepo?: string; | ||||||
| /** SHA-512 checksum of the source tarball (leave empty to use SKIP for development) */ | ||||||
| sha512sum?: string; | ||||||
| /** Runtime dependencies */ | ||||||
| depends?: string[]; | ||||||
| /** Make/build dependencies */ | ||||||
| makedepends?: string[]; | ||||||
| /** Conflicts with other packages */ | ||||||
| conflicts?: string[]; | ||||||
| /** Provides (virtual packages) */ | ||||||
| provides?: string[]; | ||||||
| } | ||||||
|
|
||||||
| function defaultSourceUrl(config: Config): string { | ||||||
| const repo = config.releaseRepo ?? config.pkgname; | ||||||
| return `https://github.com/${repo}/releases/download/v$pkgver/${config.pkgname}-$pkgver-${config.arch ?? 'x86_64'}.tar.gz`; | ||||||
| } | ||||||
|
|
||||||
| function renderPKGBUILD(config: Config, version: string): string { | ||||||
| const name = config.pkgname; | ||||||
| const arch = config.arch ?? 'x86_64'; | ||||||
| const license = config.license ?? 'MIT'; | ||||||
| const description = config.pkgdesc ?? `${name} package`; | ||||||
| const homepage = config.url ?? 'https://sh1pt.com'; | ||||||
| const sourceUrl = defaultSourceUrl(config); | ||||||
| const sha512 = config.sha512sum ?? 'SKIP'; | ||||||
| const depends = config.depends ?? []; | ||||||
| const makedepends = config.makedepends ?? []; | ||||||
| const conflicts = config.conflicts ?? []; | ||||||
| const provides = config.provides ?? []; | ||||||
|
|
||||||
| const lines = [ | ||||||
| `# Maintainer: sh1pt <noreply@sh1pt.com>`, | ||||||
| `pkgname=${name}`, | ||||||
| `pkgver=${version}`, | ||||||
| `pkgrel=1`, | ||||||
| `pkgdesc="${description}"`, | ||||||
| `arch=('${arch}')`, | ||||||
| `url="${homepage}"`, | ||||||
| `license=('${license}')`, | ||||||
| ]; | ||||||
|
|
||||||
| if (depends.length) lines.push(`depends=(${depends.map((d) => `'${d}'`).join(' ')})`); | ||||||
| if (makedepends.length) lines.push(`makedepends=(${makedepends.map((d) => `'${d}'`).join(' ')})`); | ||||||
| if (provides.length) lines.push(`provides=(${provides.map((p) => `'${p}'`).join(' ')})`); | ||||||
| if (conflicts.length) lines.push(`conflicts=(${conflicts.map((c) => `'${c}'`).join(' ')})`); | ||||||
|
|
||||||
| lines.push( | ||||||
| `source=("${name}-\${pkgver}.tar.gz::${sourceUrl}")`, | ||||||
| `sha512sums=('${sha512}')`, | ||||||
| '', | ||||||
| 'package() {', | ||||||
| ` install -Dm755 "${name}" "\${pkgdir}/usr/bin/${name}"`, | ||||||
| ` install -Dm644 LICENSE "\${pkgdir}/usr/share/licenses/${name}/LICENSE" 2>/dev/null || true`, | ||||||
| '}', | ||||||
| '', | ||||||
| ); | ||||||
|
|
||||||
| return lines.join('\n'); | ||||||
| } | ||||||
|
|
||||||
| function renderSRCINFO(config: Config, version: string): string { | ||||||
| const name = config.pkgname; | ||||||
| const arch = config.arch ?? 'x86_64'; | ||||||
| return [ | ||||||
| `pkgbase = ${name}`, | ||||||
| `\tpkgdesc = ${config.pkgdesc ?? `${name} package`}`, | ||||||
| `\tpkgver = ${version}`, | ||||||
| `\tpkgrel = 1`, | ||||||
| `\turl = ${config.url ?? 'https://sh1pt.com'}`, | ||||||
| `\tarch = ${arch}`, | ||||||
| `\tlicense = ${config.license ?? 'MIT'}`, | ||||||
| `\tsource = ${name}-${version}.tar.gz::${defaultSourceUrl(config).replace('$pkgver', version)}`, | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| `\tsha512sums = ${config.sha512sum ?? 'SKIP'}`, | ||||||
| '', | ||||||
| `pkgname = ${name}`, | ||||||
| '', | ||||||
| ].join('\n'); | ||||||
| } | ||||||
|
|
||||||
| export default defineTarget<Config>({ | ||||||
| id: 'pkg-pacman', | ||||||
| kind: 'package-manager', | ||||||
| label: 'Arch Linux AUR / Pacman', | ||||||
|
|
||||||
| async build(ctx, config) { | ||||||
| const version = ctx.version.replace(/^v/, ''); | ||||||
| const pkgbuildPath = join(ctx.outDir, 'PKGBUILD'); | ||||||
| const srcinfoPath = join(ctx.outDir, '.SRCINFO'); | ||||||
|
|
||||||
| ctx.log(`generate PKGBUILD + .SRCINFO for ${config.pkgname} v${version}`); | ||||||
| await mkdir(ctx.outDir, { recursive: true }); | ||||||
| await writeFile(pkgbuildPath, renderPKGBUILD(config, version), 'utf-8'); | ||||||
| await writeFile(srcinfoPath, renderSRCINFO(config, version), 'utf-8'); | ||||||
| ctx.log(`wrote ${pkgbuildPath}`); | ||||||
| ctx.log(`wrote ${srcinfoPath}`); | ||||||
|
|
||||||
| return { artifact: pkgbuildPath }; | ||||||
| }, | ||||||
|
|
||||||
| async ship(ctx, config) { | ||||||
| const version = ctx.version.replace(/^v/, ''); | ||||||
| ctx.log(`push ${config.pkgname} v${version} to AUR`); | ||||||
|
|
||||||
| if (ctx.dryRun) return { id: 'dry-run' }; | ||||||
|
|
||||||
| // TODO: push updated PKGBUILD + .SRCINFO to the AUR git remote | ||||||
| // AUR URL: ssh://aur@aur.archlinux.org/<pkgname>.git | ||||||
| // Requires AUR_SSH_KEY from ctx.secret('AUR_SSH_KEY') | ||||||
| return { | ||||||
| id: `${config.pkgname}@${version}`, | ||||||
| url: `https://aur.archlinux.org/packages/${config.pkgname}`, | ||||||
| }; | ||||||
| }, | ||||||
|
Comment on lines
+117
to
+130
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The non-dry-run path falls through to return |
||||||
|
|
||||||
| async status(id) { | ||||||
| const [name] = id.split('@'); | ||||||
| return { state: 'live', url: `https://aur.archlinux.org/packages/${name}` }; | ||||||
| }, | ||||||
|
|
||||||
| setup: manualSetup({ | ||||||
| label: 'Arch Linux AUR', | ||||||
| vendorDocUrl: 'https://wiki.archlinux.org/title/AUR_submission_guidelines', | ||||||
| steps: [ | ||||||
| 'Register an account at aur.archlinux.org', | ||||||
| 'Add your SSH public key in your AUR account settings', | ||||||
| 'Run: sh1pt secret set AUR_SSH_KEY <path-to-private-key>', | ||||||
| 'First time: clone your AUR package repo: ssh://aur@aur.archlinux.org/<pkgname>.git', | ||||||
| 'sh1pt will push updated PKGBUILD and .SRCINFO on each release', | ||||||
| ], | ||||||
| }), | ||||||
| }); | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"extends": "../../../tsconfig.base.json","compilerOptions": {"outDir": "dist","rootDir": "src"},"include": ["src/**/*"]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
install -Dm755 "${name}"looks for the binary in the current directory at the timepackage()runs.makepkgchanges into$srcdirbefore callingpackage(), and most GitHub Release tarballs extract into a subdirectory (e.g.myapp-3.0.1/). Without acdstep the binary won't be found andmakepkgwill fail. Referencing$srcdir/<name>-$pkgver/$name(or better, searching within$srcdir) is the standard pattern.