Skip to content

Commit c987a26

Browse files
Make extension packages portable
1 parent a9b6c57 commit c987a26

5 files changed

Lines changed: 90 additions & 14 deletions

File tree

extensions/official/icons-market/extension.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
"activationEvents": ["onIconTheme:market-icons"],
1111
"installation": {
1212
"downloadUrl": "https://athas.dev/extensions/packages/icon-theme/market/athas.icon-theme.market.tar.gz",
13-
"size": 1325,
14-
"checksum": "c60ac2bfa9d89b60ceff84dcd0fe6021e08c6c562094571b66e3d7286a52fdd5"
13+
"size": 1161,
14+
"checksum": "063f10fe0faeb317c15f386c5440360f2803057070f9d3a4cda3b4c145a64d5e"
1515
},
1616
"license": "MIT",
1717
"repository": {

extensions/official/theme-market/extension.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@
146146
],
147147
"installation": {
148148
"downloadUrl": "https://athas.dev/extensions/packages/theme/market/athas.theme.market.tar.gz",
149-
"size": 1756,
150-
"checksum": "eef547f490c83628a8fa1bd0e45d521e4bcb0c782ecbc807bb5ed2651ea90f5d"
149+
"size": 1567,
150+
"checksum": "c82ff12c6bfaa3fb54a1fe3540ca9e454b8024828a71bebefba3624df4167a38"
151151
},
152152
"license": "MIT",
153153
"repository": {

src/features/extensions/scripts/extension-workspace.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import { readdir } from "node:fs/promises";
2-
import { writeFile } from "node:fs/promises";
1+
import { createGzip } from "node:zlib";
2+
import { createWriteStream } from "node:fs";
3+
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
34
import { basename, join, relative, resolve } from "node:path";
5+
import { Readable } from "node:stream";
6+
import { pipeline } from "node:stream/promises";
47

58
export type ExtensionManifestRecord = Record<string, unknown>;
69

@@ -136,3 +139,80 @@ export async function writeExtensionManifest(
136139
) {
137140
await writeFile(manifestPath, `${stringifyManifest(manifest)}\n`);
138141
}
142+
143+
async function listPackageFiles(root: string) {
144+
const files: string[] = [];
145+
146+
async function walk(directory: string) {
147+
for (const entry of await readdir(directory, { withFileTypes: true })) {
148+
if (entry.name === ".DS_Store") continue;
149+
150+
const absolutePath = join(directory, entry.name);
151+
if (entry.isDirectory()) {
152+
await walk(absolutePath);
153+
} else if (entry.isFile()) {
154+
files.push(relative(root, absolutePath));
155+
}
156+
}
157+
}
158+
159+
await walk(root);
160+
return files.sort((a, b) => a.localeCompare(b));
161+
}
162+
163+
function writeOctalField(header: Buffer, offset: number, length: number, value: number) {
164+
const octal = value.toString(8).padStart(length - 1, "0");
165+
header.write(octal, offset, length - 1, "ascii");
166+
header[offset + length - 1] = 0;
167+
}
168+
169+
function writeTarHeader(path: string, size: number, mode: number) {
170+
const header = Buffer.alloc(512, 0);
171+
const normalizedPath = path.replace(/\\/g, "/");
172+
173+
if (Buffer.byteLength(normalizedPath) > 100) {
174+
throw new Error(`Packaged extension path is too long for portable tar: ${normalizedPath}`);
175+
}
176+
177+
header.write(normalizedPath, 0, 100, "utf8");
178+
writeOctalField(header, 100, 8, mode & 0o777);
179+
writeOctalField(header, 108, 8, 0);
180+
writeOctalField(header, 116, 8, 0);
181+
writeOctalField(header, 124, 12, size);
182+
writeOctalField(header, 136, 12, 1577836800);
183+
header.fill(" ", 148, 156);
184+
header[156] = "0".charCodeAt(0);
185+
header.write("ustar", 257, 6, "ascii");
186+
header.write("00", 263, 2, "ascii");
187+
188+
let checksum = 0;
189+
for (const byte of header) checksum += byte;
190+
const checksumText = checksum.toString(8).padStart(6, "0");
191+
header.write(checksumText, 148, 6, "ascii");
192+
header[154] = 0;
193+
header[155] = 0x20;
194+
195+
return header;
196+
}
197+
198+
export async function writeStableTarGz(root: string, packagePath: string) {
199+
const chunks: Buffer[] = [];
200+
201+
for (const file of await listPackageFiles(root)) {
202+
const absolutePath = join(root, file);
203+
const fileStats = await stat(absolutePath);
204+
const contents = await readFile(absolutePath);
205+
chunks.push(writeTarHeader(file, contents.length, fileStats.mode));
206+
chunks.push(contents);
207+
208+
const padding = (512 - (contents.length % 512)) % 512;
209+
if (padding > 0) {
210+
chunks.push(Buffer.alloc(padding, 0));
211+
}
212+
}
213+
214+
chunks.push(Buffer.alloc(1024, 0));
215+
const output = createWriteStream(packagePath);
216+
const gzip = createGzip({ level: 9 });
217+
await pipeline(Readable.from(chunks), gzip, output);
218+
}

src/features/extensions/scripts/package-database-sidecars.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getGeneratedCdnPath,
1414
listExtensionFolders,
1515
writeExtensionManifest,
16+
writeStableTarGz,
1617
} from "./extension-workspace";
1718

1819
const cdnBaseUrl = process.env.EXTENSIONS_CDN_BASE_URL || "https://athas.dev/extensions";
@@ -72,10 +73,7 @@ async function createPackage(params: {
7273
await chmod(targetBinary, 0o755);
7374

7475
await mkdir(dirname(params.packagePath), { recursive: true });
75-
await $`find ${tempDir} -exec touch -t 202001010000 {} +`;
76-
await $`find . -type f -print | LC_ALL=C sort | tar --no-xattrs --owner=0 --group=0 --numeric-owner -cf - -C ${tempDir} -T - | gzip -n > ${params.packagePath}`.cwd(
77-
tempDir,
78-
);
76+
await writeStableTarGz(tempDir, params.packagePath);
7977
} finally {
8078
await rm(tempDir, { recursive: true, force: true });
8179
}

src/features/extensions/scripts/package-extensions.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getGeneratedCdnPath,
1313
listExtensionFolders,
1414
writeExtensionManifest,
15+
writeStableTarGz,
1516
} from "./extension-workspace";
1617

1718
const cdnBaseUrl = process.env.EXTENSIONS_CDN_BASE_URL || "https://athas.dev/extensions";
@@ -48,10 +49,7 @@ async function createStablePackage(
4849
`${JSON.stringify(packagedManifest, null, 2)}\n`,
4950
);
5051

51-
await $`find ${tempDir} -exec touch -t 202001010000 {} +`;
52-
await $`find . -type f -print | LC_ALL=C sort | tar --no-xattrs --owner=0 --group=0 --numeric-owner -cf - -C ${tempDir} -T - | gzip -n > ${packagePath}`.cwd(
53-
tempDir,
54-
);
52+
await writeStableTarGz(tempDir, packagePath);
5553
} finally {
5654
await rm(tempDir, { recursive: true, force: true });
5755
}

0 commit comments

Comments
 (0)