Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/targets/pkg-pacman/README.md
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.
23 changes: 23 additions & 0 deletions packages/targets/pkg-pacman/package.json
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:*"
},
Comment on lines +11 to +13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Wrong workspace dependency name

The package declares @profullstack/sh1pt-core as a dependency, but the actual package in this monorepo is @sh1pt/core (see packages/core/package.json). The workspace:* protocol will fail to resolve because no package named @profullstack/sh1pt-core exists in the workspace. The same mismatch applies to the import in src/index.tsimport { defineTarget, manualSetup } from '@profullstack/sh1pt-core' will throw a module-not-found error at both build and test time.

The package's own name (@profullstack/sh1pt-target-pkg-pacman) also diverges from every other target in this repo, which use the @sh1pt/target-* scope.

"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"]
}
53 changes: 53 additions & 0 deletions packages/targets/pkg-pacman/src/index.test.ts
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' });
});
});
148 changes: 148 additions & 0 deletions packages/targets/pkg-pacman/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { defineTarget, manualSetup } from '@profullstack/sh1pt-core';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 manualSetup does not exist anywhere in the codebase — it is not defined or re-exported in packages/core/src/target.ts or packages/core/src/index.ts. Importing it will throw at module-load time, making the adapter completely non-functional. No other adapter in the repo uses this helper; they simply omit a setup property (which is also not part of the Target<Config> interface, so adding it would be flagged by TypeScript's excess-property check).

Suggested change
import { defineTarget, manualSetup } from '@profullstack/sh1pt-core';
import { defineTarget } from '@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)}`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 defaultSourceUrl embeds $pkgver twice — once in v$pkgver and once in ${pkgname}-$pkgver-…. String.prototype.replace without a /g flag only substitutes the first occurrence, so the second $pkgver stays verbatim in the source = line. Because .SRCINFO is parsed as plain key-value data (not evaluated as Bash), makepkg / aurpublish will see a literal $pkgver in the download URL and fail to fetch the tarball.

Suggested change
`\tsource = ${name}-${version}.tar.gz::${defaultSourceUrl(config).replace('$pkgver', version)}`,
`\tsource = ${name}-${version}.tar.gz::${defaultSourceUrl(config).replaceAll('$pkgver', version)}`,

`\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}`,
};
},

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',
],
}),
});
Comment on lines +119 to +148
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 setup property is not part of the Target<Config> interface

The Target<Config> interface in packages/core/src/target.ts has no setup field. TypeScript's excess-property check will reject the object literal passed to defineTarget<Config>({…}) with "Object literal may only specify known properties." Additionally, manualSetup (which would populate this field) does not exist in the core package, so this block will fail both at type-check and at runtime. The setup key should be removed until a setup API is added to the core interface and a corresponding implementation is exported.

1 change: 1 addition & 0 deletions packages/targets/pkg-pacman/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"extends": "../../../tsconfig.base.json","compilerOptions": {"outDir": "dist","rootDir": "src"},"include": ["src/**/*"]}
Loading