Skip to content
This repository has been archived by the owner on Nov 21, 2023. It is now read-only.

Brotli compression #7

Closed
wants to merge 3 commits into from
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
36 changes: 36 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
},
"dependencies": {
"archiver": "^5.2.0",
"brotli": "^1.3.2",
"commander": "^7.0.0",
"crypto-random-string": "^3.3.0",
"execa": "^4.1.0",
"fs-extra": "^9.1.0"
},
"devDependencies": {
"@types/archiver": "^5.1.0",
"@types/brotli": "^1.3.0",
"@types/fs-extra": "^9.0.7",
"@types/jest": "^26.0.20",
"@types/node": "^14.14.26",
Expand Down
39 changes: 33 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
#!/usr/bin/env node

import stream from "stream";
import process from "process";
import path from "path";
import os from "os";
import fs from "fs-extra";
import execa from "execa";
import archiver from "archiver";
import brotli from "brotli";
import cryptoRandomString from "crypto-random-string";
import commander from "commander";

export default async function caxa({
directory,
command,
output,
format,
}: {
directory: string;
command: string[];
output: string;
format: string;
}): Promise<void> {
if (
!(await fs.pathExists(directory)) ||
!(await fs.lstat(directory)).isDirectory()
)
throw new Error(`The path to package isn’t a directory: ‘${directory}’.`);

if (["gzip", "brotli"].indexOf(format) === -1)
throw new Error(`Format must be ‘gzip’ or ‘brotli’.`);

const identifier = path.join(
path.basename(path.basename(output, ".app"), ".exe"),
cryptoRandomString({ length: 10, type: "alphanumeric" }).toLowerCase()
Expand Down Expand Up @@ -87,17 +94,34 @@ export default async function caxa({
),
output
);
const archive = archiver("tar", { gzip: true });
const archiveStream = fs.createWriteStream(output, { flags: "a" });

const rawData: any[] = [];
const rawStream = new stream.Writable({
write: (chunk, _, done) => {
rawData.push(chunk);
done();
},
});

const archive = archiver("tar", { gzip: format === "gzip" });
const archiveStream = (format === "gzip") ? fs.createWriteStream(output, { flags: "a" }) : rawStream;

archive.pipe(archiveStream);
archive.directory(appDirectory, false);
await archive.finalize();

// FIXME: Use ‘stream/promises’ when Node.js 16 lands, because then an LTS version will have the feature: await stream.finished(archiveStream);
await new Promise((resolve, reject) => {
let finishPromise = new Promise((resolve, reject) => {
archiveStream.on("finish", resolve);
archiveStream.on("error", reject);
});
await fs.appendFile(output, "\n" + JSON.stringify({ identifier, command }));

await archive.finalize();
await finishPromise;

if (format === "brotli")
await fs.appendFile(output, brotli.compress(Buffer.concat(rawData)));

await fs.appendFile(output, "\n" + JSON.stringify({ identifier, command, format }));
}
}

Expand All @@ -116,6 +140,7 @@ if (require.main === module)
"-o, --output <output>",
"The path at which to produce the executable. Overwrites existing files/folders. On Windows must end in ‘.exe’. On macOS may end in ‘.app’ to generate a macOS Application Bundle."
)
.option("-f, --format <format>", "The compression format to use, either ‘gzip’ (default) or ‘brotli’. Brotli takes several minutes to compress, but generates an output executable at least 6MB smaller.", "gzip")
.version(require("../package.json").version)
.addHelpText(
"after",
Expand All @@ -137,13 +162,15 @@ Examples:
directory,
command,
output,
format,
}: {
directory: string;
command: string[];
output: string;
format: string;
}) => {
try {
await caxa({ directory, command, output });
await caxa({ directory, command, output, format });
} catch (error) {
console.error(error.message);
process.exit(1);
Expand Down
5 changes: 5 additions & 0 deletions stubs/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/leafac/caxa/stubs

go 1.16

require github.com/google/brotli/go/cbrotli v0.0.0-20210127140805-63be8a994019
2 changes: 2 additions & 0 deletions stubs/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/google/brotli/go/cbrotli v0.0.0-20210127140805-63be8a994019 h1:XYW4NntIMcMzsu+XjMKziKuSgthVc/nSnDrFu/iJuzA=
github.com/google/brotli/go/cbrotli v0.0.0-20210127140805-63be8a994019/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
23 changes: 17 additions & 6 deletions stubs/stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"regexp"
"strings"
"time"
"github.com/google/brotli/go/cbrotli"
)

func main() {
Expand All @@ -38,12 +39,15 @@ func main() {
var footer struct {
Identifier string `json:"identifier"`
Command []string `json:"command"`
Format string `json:"format"`
}
if err := json.Unmarshal(footerString, &footer); err != nil {
log.Fatalf("caxa stub: Failed to parse JSON in footer: %v", err)
}

appDirectory := path.Join(os.TempDir(), "caxa", footer.Identifier)
log.Printf("caxa stub: appDirectory: %v", appDirectory)

appDirectoryFileInfo, err := os.Stat(appDirectory)
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Fatalf("caxa stub: Failed to find information about caxa directory: %v", err)
Expand All @@ -63,7 +67,18 @@ func main() {
}
archive := executable[archiveIndex+len(archiveSeparator) : footerIndex]

if err := Untar(bytes.NewReader(archive), appDirectory); err != nil {
var reader io.Reader;

if footer.Format == "brotli" {
reader = cbrotli.NewReader(bytes.NewReader(archive))
} else {
reader, err = gzip.NewReader(bytes.NewReader(archive))
if err != nil {
log.Fatalf("requires gzip compressed body: %v", err)
}
}

if err := Untar(reader, appDirectory); err != nil {
log.Fatalf("caxa stub: Failed to uncompress archive: %v", err)
}
}
Expand Down Expand Up @@ -131,11 +146,7 @@ func untar(r io.Reader, dir string) (err error) {
// log.Printf("error extracting tarball into %s after %d files, %d dirs, %v: %v", dir, nFiles, len(madeDir), td, err)
// }
// }()
zr, err := gzip.NewReader(r)
if err != nil {
return fmt.Errorf("requires gzip-compressed body: %v", err)
}
tr := tar.NewReader(zr)
tr := tar.NewReader(r)
loggedChtimesError := false
for {
f, err := tr.Next()
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
"sourceMap": true,

"strict": true
}
},
"exclude": ["examples"]
}