From 389999b096eaaf83578fbc3cbc1e61bd49898898 Mon Sep 17 00:00:00 2001 From: culpen90 Date: Sun, 21 Jun 2026 15:29:49 -0400 Subject: [PATCH] Add QRX extension packaging --- .gitignore | 5 ++- README.md | 13 +++++-- THIRD_PARTY_NOTICES.md | 3 +- Tools/package_qrx.py | 83 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 Tools/package_qrx.py diff --git a/.gitignore b/.gitignore index 0623567..ea54af6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .DS_Store +__pycache__/ +*.pyc *.zip - +*.qrx +dist/ diff --git a/README.md b/README.md index 13b37da..ea8cf4a 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # Quartz Ad Blocker -Optional ad blocking for Quartz as a WebExtension package. +Optional ad blocking for Quartz as a single `.qrx` WebExtension package. ## Install in Quartz -1. Build or download this repository. +1. Build or download `QuartzAdBlocker.qrx`. 2. Open Quartz on macOS 15.4 or later. 3. Choose **Extensions > Install Extension...**. -4. Select the `Extension` directory in this repository, or a ZIP archive of that directory. +4. Select the `.qrx` package. Quartz loads installed extensions before the first page navigation on later launches. @@ -25,3 +25,10 @@ python3 Tools/generate_rules.py The generator reads `Filters/` and writes `Extension/rules/rules.json` plus `Extension/rules/metadata.json`. +## Build QRX Package + +```sh +python3 Tools/package_qrx.py +``` + +The packager writes `dist/QuartzAdBlocker.qrx`. The `.qrx` file is a Zip-compatible extension package with `manifest.json` at the archive root, which is the single file Quartz installs. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 79cde95..c0dd07e 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -8,5 +8,4 @@ This repository contains filter assets and generated rules derived from the form - Original asset: `uBlock0_1.71.0.chromium.zip` - License: GPL-3.0-or-later, preserved at `Filters/uBlockOrigin/LICENSE.txt` -The generated `Extension/rules/rules.json` file is produced from the compatible network-filter subset of the filter files in `Filters/`. - +The generated `Extension/rules/rules.json` file is produced from the compatible network-filter subset of the filter files in `Filters/`. The `.qrx` package is a Zip-compatible archive of the generated extension files. diff --git a/Tools/package_qrx.py b/Tools/package_qrx.py new file mode 100644 index 0000000..32fd0af --- /dev/null +++ b/Tools/package_qrx.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +import argparse +import hashlib +import json +from pathlib import Path +import sys +from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo + + +ROOT = Path(__file__).resolve().parents[1] +EXTENSION_ROOT = ROOT / "Extension" +DEFAULT_OUTPUT = ROOT / "dist" / "QuartzAdBlocker.qrx" +ZIP_TIMESTAMP = (1980, 1, 1, 0, 0, 0) + + +def iter_extension_files(): + for path in sorted(EXTENSION_ROOT.rglob("*")): + if path.is_file() and path.name != ".DS_Store": + yield path + + +def load_manifest(): + manifest_path = EXTENSION_ROOT / "manifest.json" + try: + with manifest_path.open(encoding="utf-8") as handle: + manifest = json.load(handle) + except FileNotFoundError: + sys.exit(f"Missing required extension manifest: {manifest_path}") + except json.JSONDecodeError as error: + sys.exit(f"Invalid JSON in {manifest_path}: {error}") + + required_fields = ("manifest_version", "name", "version") + missing_fields = [field for field in required_fields if field not in manifest] + if missing_fields: + missing = ", ".join(missing_fields) + sys.exit(f"Extension manifest is missing required field(s): {missing}") + + return manifest + + +def make_zip_info(path): + archive_name = path.relative_to(EXTENSION_ROOT).as_posix() + info = ZipInfo(archive_name, ZIP_TIMESTAMP) + info.compress_type = ZIP_DEFLATED + info.external_attr = 0o644 << 16 + return info + + +def package_qrx(output_path): + manifest = load_manifest() + files = list(iter_extension_files()) + if not files: + sys.exit(f"No files found to package in {EXTENSION_ROOT}") + + output_path.parent.mkdir(parents=True, exist_ok=True) + with ZipFile(output_path, "w") as archive: + for path in files: + archive.writestr(make_zip_info(path), path.read_bytes()) + + digest = hashlib.sha256(output_path.read_bytes()).hexdigest() + print(f"Wrote {output_path}") + print(f"Packaged {len(files)} files for {manifest['name']} {manifest['version']}") + print(f"SHA-256: {digest}") + + +def main(): + parser = argparse.ArgumentParser( + description="Package the Quartz Ad Blocker extension as a single .qrx file." + ) + parser.add_argument( + "-o", + "--output", + type=Path, + default=DEFAULT_OUTPUT, + help=f"Destination .qrx path. Defaults to {DEFAULT_OUTPUT}", + ) + args = parser.parse_args() + + package_qrx(args.output.resolve()) + + +if __name__ == "__main__": + main()