From 55a6c0a824e7a6ee568cbf5efa546a90303dffe0 Mon Sep 17 00:00:00 2001 From: Sameer Srivastava Date: Tue, 2 Jun 2026 14:43:25 +0200 Subject: [PATCH 1/3] Include chipmunk-cli in release workflow Include chipmunk-cli in release workflow Remove README.md from tar release artifact --- .github/workflows/release.yml | 8 +-- development/scripts/release_app.py | 87 +++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 98746152d..1a50c9c66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ permissions: jobs: create_release: - name: Create app release + name: Create release runs-on: ubuntu-latest outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} @@ -30,7 +30,7 @@ jobs: name: ${{ github.ref_name }} build_release: - name: Build app release + name: Build release needs: create_release runs-on: ${{ matrix.os }} strategy: @@ -135,7 +135,7 @@ jobs: security set-keychain-settings build.keychain security unlock-keychain -p "$KEYCHAIN_PWD" build.keychain security find-identity -v -p codesigning build.keychain - - name: Build app release + - name: Build release shell: bash run: | # release_app.py owns the artifact layout. macOS needs signing and @@ -145,7 +145,7 @@ jobs: else just release fi - - name: List app release files + - name: List release files working-directory: ./target/dist run: ls - name: Upload release asset diff --git a/development/scripts/release_app.py b/development/scripts/release_app.py index 0e703afb3..54539c44d 100755 --- a/development/scripts/release_app.py +++ b/development/scripts/release_app.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Build and package the Chipmunk app. +"""Build and package the Chipmunk app and CLI. This script is the release artifact's source of truth for CI and local builds. It deliberately keeps the public portable artifact names compatible with the @@ -40,7 +40,7 @@ def main(): parser = argparse.ArgumentParser( - description="Build and package the Chipmunk native app." + description="Build and package the Chipmunk native app and CLI." ) parser.add_argument( "--code-sign", @@ -51,8 +51,10 @@ def main(): clean_release() build_app() + build_cli() version = app_version() + cli_version_value = cli_version() if is_macos(): artifacts = package_macos(version, code_sign=args.code_sign) elif is_windows(): @@ -60,6 +62,8 @@ def main(): else: artifacts = [package_portable(version)] + artifacts.append(package_cli_portable(cli_version_value)) + for artifact in artifacts: print("Chipmunk release artifact created: {}".format(artifact)) return 0 @@ -81,6 +85,22 @@ def build_app(): ) +def build_cli(): + """Build the standalone CLI binary that ships as its own release artifact.""" + run( + [ + "cargo", + "build", + "--release", + "--locked", + "--manifest-path", + str(cli_manifest_path()), + ], + cwd=workspace_root(), + error="Building Chipmunk CLI failed", + ) + + def package_portable(version): """Create the Linux/Windows portable archive. @@ -92,9 +112,8 @@ def package_portable(version): archive_root = "chipmunk@{}-{}-portable".format(version, platform_name()) staging_dir = app_release_path() / archive_root - staging_dir.mkdir(parents=True, exist_ok=True) + reset_staging_dir(staging_dir) shutil.copy2(app_binary_path(), staging_dir / app_binary_name()) - shutil.copy2(repo_readme_path(), staging_dir / "README.md") write_release_manifest(staging_dir) archive = app_release_path() / "{}.tgz".format(archive_root) @@ -103,6 +122,27 @@ def package_portable(version): return archive +def package_cli_portable(version): + """Create the portable CLI archive using the legacy flat layout.""" + archive_root = "chipmunk-cli@{}-{}-portable".format(version, platform_name()) + staging_dir = app_release_path() / archive_root + + reset_staging_dir(staging_dir) + shutil.copy2(cli_binary_path(), staging_dir / cli_binary_name()) + + archive = app_release_path() / "{}.tgz".format(archive_root) + write_flat_tgz_archive(staging_dir, archive) + + return archive + + +def reset_staging_dir(staging_dir): + """Create a fresh staging directory so stale files never enter an archive.""" + if staging_dir.exists(): + shutil.rmtree(staging_dir) + staging_dir.mkdir(parents=True) + + def write_release_manifest(staging_dir): """Write the file list consumed by the legacy self-updater.""" entries = [".release"] @@ -408,8 +448,24 @@ def app_version(): return read_workspace_package_version(manifest) +def cli_version(): + """Read the CLI artifact version from crates/cli/Cargo.toml.""" + manifest = cli_manifest_path() + if tomllib is not None: + with manifest.open("rb") as file: + cargo_toml = tomllib.load(file) + return cargo_toml["package"]["version"] + + return read_package_version(manifest) + + def read_workspace_package_version(manifest): """Fallback TOML reader for Python versions without tomllib.""" + return read_package_version(manifest, section="workspace.package") + + +def read_package_version(manifest, section="package"): + """Fallback TOML version reader for simple package tables.""" current_section = "" for raw_line in manifest.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() @@ -418,10 +474,10 @@ def read_workspace_package_version(manifest): if line.startswith("[") and line.endswith("]"): current_section = line[1:-1] continue - if current_section == "workspace.package" and line.startswith("version"): + if current_section == section and line.startswith("version"): _, value = line.split("=", 1) return value.strip().strip('"') - raise RuntimeError("Could not find workspace.package.version in {}".format(manifest)) + raise RuntimeError("Could not find {}.version in {}".format(section, manifest)) def write_info_plist(path, version): @@ -582,10 +638,18 @@ def app_root(): return repo_root() / "crates" / "app" +def cli_root(): + return repo_root() / "crates" / "cli" + + def app_manifest_path(): return app_root() / "Cargo.toml" +def cli_manifest_path(): + return cli_root() / "Cargo.toml" + + def app_release_path(): return repo_root() / "target" / "dist" @@ -605,6 +669,17 @@ def app_binary_name(): return "chipmunk.exe" if is_windows() else "chipmunk" +def cli_binary_path(): + path = workspace_root() / "target" / "release" / cli_binary_name() + if not path.exists(): + raise RuntimeError("Chipmunk CLI binary doesn't exist: {}".format(path)) + return path + + +def cli_binary_name(): + return "chipmunk-cli.exe" if is_windows() else "chipmunk-cli" + + def repo_readme_path(): return repo_root() / "README.md" From 49961d6fbd57331ff49a1b541bb45fb324e60134 Mon Sep 17 00:00:00 2001 From: Sameer Srivastava Date: Tue, 2 Jun 2026 14:46:25 +0200 Subject: [PATCH 2/3] Remove the unused .release file --- development/scripts/release_app.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/development/scripts/release_app.py b/development/scripts/release_app.py index 54539c44d..295b4cea7 100755 --- a/development/scripts/release_app.py +++ b/development/scripts/release_app.py @@ -114,7 +114,6 @@ def package_portable(version): reset_staging_dir(staging_dir) shutil.copy2(app_binary_path(), staging_dir / app_binary_name()) - write_release_manifest(staging_dir) archive = app_release_path() / "{}.tgz".format(archive_root) write_flat_tgz_archive(staging_dir, archive) @@ -143,18 +142,6 @@ def reset_staging_dir(staging_dir): staging_dir.mkdir(parents=True) -def write_release_manifest(staging_dir): - """Write the file list consumed by the legacy self-updater.""" - entries = [".release"] - entries.extend( - sorted(path.name for path in staging_dir.iterdir() if path.name != ".release") - ) - (staging_dir / ".release").write_text( - "{}\n".format("\n".join(entries)), - encoding="utf-8", - ) - - def write_flat_tgz_archive(source_dir, archive): """Tar the staged files at archive root, without a wrapper directory.""" with tarfile.open(archive, "w:gz") as tar: From e437d9ed2d016e9b20eb70eb113a26e0f3668654 Mon Sep 17 00:00:00 2001 From: Sameer Srivastava Date: Mon, 8 Jun 2026 09:37:45 +0200 Subject: [PATCH 3/3] Add debian and rpm packaging --- .github/workflows/release.yml | 6 + crates/app/data/linux/chipmunk.desktop | 12 +- development/__init__.py | 1 + development/packaging/__init__.py | 1 + development/packaging/linux/__init__.py | 5 + development/packaging/linux/packages.py | 381 ++++++++++++++++++ .../linux/templates/debian/control.in | 15 + .../linux/templates/debian/copyright.in | 22 + .../templates/debian/shlibdeps-control.in | 12 + .../linux/templates/rpm/chipmunk.spec.in | 34 ++ development/scripts/release_app.py | 101 ++++- 11 files changed, 566 insertions(+), 24 deletions(-) mode change 100755 => 100644 crates/app/data/linux/chipmunk.desktop create mode 100644 development/__init__.py create mode 100644 development/packaging/__init__.py create mode 100644 development/packaging/linux/__init__.py create mode 100644 development/packaging/linux/packages.py create mode 100644 development/packaging/linux/templates/debian/control.in create mode 100644 development/packaging/linux/templates/debian/copyright.in create mode 100644 development/packaging/linux/templates/debian/shlibdeps-control.in create mode 100644 development/packaging/linux/templates/rpm/chipmunk.spec.in diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a50c9c66..6421cce06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,6 +71,10 @@ jobs: libxcb-shape0-dev \ libxcb-xfixes0-dev \ libxcb-xkb-dev \ + binutils \ + desktop-file-utils \ + dpkg-dev \ + rpm \ pkg-config - name: Install rust uses: dtolnay/rust-toolchain@stable @@ -158,5 +162,7 @@ jobs: # public release assets. files: | ./target/dist/*.tgz + ./target/dist/*.deb + ./target/dist/*.rpm ./target/dist/*.msi ./target/dist/*.pkg diff --git a/crates/app/data/linux/chipmunk.desktop b/crates/app/data/linux/chipmunk.desktop old mode 100755 new mode 100644 index 9136494a5..c2b1a87e8 --- a/crates/app/data/linux/chipmunk.desktop +++ b/crates/app/data/linux/chipmunk.desktop @@ -2,14 +2,14 @@ Name=Chipmunk Comment=Log viewer tool GenericName=Log viewer tool -Exec=chipmunk %f -Icon=chipmunk.png +Exec=chipmunk files %F +Icon=chipmunk Type=Application -Categories=Utility;Tools; +Categories=Utility;Development; Actions=new-empty-window; -Keywords=chipmunk; +Keywords=chipmunk;log;viewer;trace;dlt; [Desktop Action new-empty-window] Name=New Empty Window -Exec=chipmunk %f -Icon=chipmunk.png +Exec=chipmunk +Icon=chipmunk diff --git a/development/__init__.py b/development/__init__.py new file mode 100644 index 000000000..1841b5026 --- /dev/null +++ b/development/__init__.py @@ -0,0 +1 @@ +"""Development tooling for the Chipmunk workspace.""" diff --git a/development/packaging/__init__.py b/development/packaging/__init__.py new file mode 100644 index 000000000..9aeeb32e9 --- /dev/null +++ b/development/packaging/__init__.py @@ -0,0 +1 @@ +"""Release packaging helpers.""" diff --git a/development/packaging/linux/__init__.py b/development/packaging/linux/__init__.py new file mode 100644 index 000000000..83ccc7141 --- /dev/null +++ b/development/packaging/linux/__init__.py @@ -0,0 +1,5 @@ +"""Linux installer packaging entrypoints.""" + +from .packages import LinuxPackageConfig, package_linux_installers + +__all__ = ["LinuxPackageConfig", "package_linux_installers"] diff --git a/development/packaging/linux/packages.py b/development/packaging/linux/packages.py new file mode 100644 index 000000000..bb719facf --- /dev/null +++ b/development/packaging/linux/packages.py @@ -0,0 +1,381 @@ +"""Build Linux DEB and RPM installers for the native Chipmunk app.""" + +import platform +import re +import shutil +import subprocess +import tarfile +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, Iterable, List, Optional, Tuple + + +PACKAGE_NAME = "chipmunk" +DEB_REVISION = "1" +RPM_STABLE_RELEASE = "1" +RPM_PRERELEASE_PREFIX = "0" +DEB_STATIC_DEPENDS = ("hicolor-icon-theme",) +ICON_SIZES = ("16", "24", "32", "64", "128", "256", "512") +SEMVER_RE = re.compile( + r"^(?P\d+\.\d+\.\d+)" + r"(?:-(?P[0-9A-Za-z][0-9A-Za-z.-]*))?" + r"(?:\+(?P[0-9A-Za-z][0-9A-Za-z.-]*))?$" +) + + +@dataclass(frozen=True) +class LinuxPackageConfig: + version: str + dist_dir: Path + app_binary: Path + desktop_file: Path + icon_dir: Path + readme: Path + license_file: Path + + +def package_linux_installers(config: LinuxPackageConfig) -> List[Path]: + """Create Linux installer packages and return their artifact paths.""" + ensure_linux_host() + + work_dir = config.dist_dir / "linux-packaging" + reset_dir(work_dir) + + return [ + build_deb(config, work_dir / "deb"), + build_rpm(config, work_dir / "rpm"), + ] + + +def build_deb(config: LinuxPackageConfig, work_dir: Path) -> Path: + ensure_tools(["desktop-file-validate", "dpkg", "dpkg-deb", "dpkg-shlibdeps"]) + + root = work_dir / "debian" / PACKAGE_NAME + stage_install_tree(config, root, package_format="deb") + validate_desktop_file(root) + + control_dir = root / "DEBIAN" + make_dir(control_dir, 0o755) + + arch = deb_architecture() + depends = deb_control_depends( + deb_dependencies(root / "usr" / "bin" / PACKAGE_NAME, work_dir) + ) + control = render_template( + "debian/control.in", + { + "architecture": arch, + "depends_field": "Depends: {}\n".format(depends) if depends else "", + "installed_size": str(installed_size_kib(root)), + "version": debian_version(config.version), + }, + ) + write_text(control_dir / "control", control, 0o644) + + artifact = config.dist_dir / "chipmunk@{}-linux-{}.deb".format( + config.version, arch + ) + run( + ["dpkg-deb", "--build", "--root-owner-group", str(root), str(artifact)], + error="Creating Debian package failed", + ) + return artifact + + +def build_rpm(config: LinuxPackageConfig, work_dir: Path) -> Path: + ensure_tools(["desktop-file-validate", "rpm", "rpmbuild"]) + + root = work_dir / "root" + stage_install_tree(config, root, package_format="rpm") + validate_desktop_file(root) + + sources_dir = work_dir / "SOURCES" + specs_dir = work_dir / "SPECS" + make_dir(sources_dir, 0o755) + make_dir(specs_dir, 0o755) + + source = sources_dir / "chipmunk-linux-install.tar.gz" + write_install_tar(root, source) + + rpm_version, rpm_release = rpm_version_release(config.version) + spec = render_template( + "rpm/chipmunk.spec.in", + { + "release": rpm_release, + "source": source.name, + "version": rpm_version, + }, + ) + spec_path = specs_dir / "chipmunk.spec" + write_text(spec_path, spec, 0o644) + + run( + [ + "rpmbuild", + "--define", + "_topdir {}".format(work_dir), + "-bb", + str(spec_path), + ], + error="Creating RPM package failed", + ) + + built = sorted((work_dir / "RPMS").rglob("chipmunk-*.rpm")) + if not built: + raise RuntimeError("RPM build finished without producing a package") + + arch = rpm_architecture() + artifact = config.dist_dir / "chipmunk@{}-linux-{}.rpm".format( + config.version, arch + ) + shutil.copy2(built[0], artifact) + artifact.chmod(0o644) + return artifact + + +def stage_install_tree( + config: LinuxPackageConfig, root: Path, package_format: str +) -> None: + reset_dir(root) + + install_file(config.app_binary, root / "usr" / "bin" / PACKAGE_NAME, 0o755) + install_file( + config.desktop_file, + root / "usr" / "share" / "applications" / "chipmunk.desktop", + 0o644, + ) + + for size in ICON_SIZES: + install_file( + config.icon_dir / "{}.png".format(size), + root + / "usr" + / "share" + / "icons" + / "hicolor" + / "{}x{}".format(size, size) + / "apps" + / "chipmunk.png", + 0o644, + ) + + doc_dir = root / "usr" / "share" / "doc" / PACKAGE_NAME + install_file(config.readme, doc_dir / "README.md", 0o644) + + if package_format == "deb": + copyright_text = render_template( + "debian/copyright.in", + {"source": "https://github.com/esrlabs/chipmunk"}, + ) + write_text(doc_dir / "copyright", copyright_text, 0o644) + elif package_format == "rpm": + install_file( + config.license_file, + root / "usr" / "share" / "licenses" / PACKAGE_NAME / "LICENSE.txt", + 0o644, + ) + else: + raise RuntimeError("Unknown Linux package format: {}".format(package_format)) + + normalize_directory_modes(root) + + +def deb_dependencies(binary: Path, work_dir: Path) -> str: + debian_dir = work_dir / "debian" + make_dir(debian_dir, 0o755) + write_text( + debian_dir / "control", + render_template("debian/shlibdeps-control.in", {}), + 0o644, + ) + + result = run_capture( + ["dpkg-shlibdeps", "-O", "-e{}".format(binary)], + cwd=work_dir, + error="Resolving Debian shared-library dependencies failed", + ) + + for line in result.stdout.splitlines(): + if line.startswith("shlibs:Depends="): + return line.split("=", 1)[1].strip() + return "" + + +def deb_control_depends(dynamic_depends: str) -> str: + depends = list(DEB_STATIC_DEPENDS) + if dynamic_depends: + depends.append(dynamic_depends) + return ", ".join(depends) + + +def debian_version(version: str) -> str: + core, prerelease, build = split_semver(version) + upstream = core + if prerelease: + upstream += "~{}".format(sanitize_debian_part(prerelease)) + if build: + upstream += "+{}".format(sanitize_debian_part(build)) + return "{}-{}".format(upstream, DEB_REVISION) + + +def rpm_version_release(version: str) -> Tuple[str, str]: + core, prerelease, build = split_semver(version) + release_parts = [] + if prerelease: + release_parts.extend([RPM_PRERELEASE_PREFIX, sanitize_rpm_part(prerelease)]) + else: + release_parts.append(RPM_STABLE_RELEASE) + if build: + release_parts.append(sanitize_rpm_part(build)) + return core, ".".join(part for part in release_parts if part) + + +def split_semver(version: str) -> Tuple[str, Optional[str], Optional[str]]: + match = SEMVER_RE.match(version) + if not match: + raise RuntimeError("Unsupported package version: {}".format(version)) + return ( + match.group("core"), + match.group("prerelease"), + match.group("build"), + ) + + +def sanitize_debian_part(value: str) -> str: + return sanitize(value, allowed=r"A-Za-z0-9.+~", replacement=".") + + +def sanitize_rpm_part(value: str) -> str: + return sanitize(value, allowed=r"A-Za-z0-9._", replacement=".") + + +def sanitize(value: str, allowed: str, replacement: str) -> str: + sanitized = re.sub(r"[^{}]+".format(allowed), replacement, value) + sanitized = re.sub(r"\.+", ".", sanitized).strip(".") + if not sanitized: + raise RuntimeError("Version segment became empty after sanitizing: {}".format(value)) + return sanitized + + +def deb_architecture() -> str: + return run_capture( + ["dpkg", "--print-architecture"], + error="Resolving Debian architecture failed", + ).stdout.strip() + + +def rpm_architecture() -> str: + arch = run_capture( + ["rpm", "--eval", "%{_target_cpu}"], + error="Resolving RPM architecture failed", + ).stdout.strip() + return arch or platform.machine() + + +def installed_size_kib(root: Path) -> int: + total = 0 + for path in root.rglob("*"): + if path.is_file() and "DEBIAN" not in path.relative_to(root).parts: + total += path.stat().st_size + return max(1, (total + 1023) // 1024) + + +def validate_desktop_file(root: Path) -> None: + run( + [ + "desktop-file-validate", + str(root / "usr" / "share" / "applications" / "chipmunk.desktop"), + ], + error="Validating Linux desktop file failed", + ) + + +def write_install_tar(root: Path, archive: Path) -> None: + with tarfile.open(archive, "w:gz") as tar: + for path in sorted(root.iterdir(), key=lambda item: item.name): + tar.add(path, arcname=path.name, filter=root_owned_tar_info) + archive.chmod(0o644) + + +def root_owned_tar_info(tar_info: tarfile.TarInfo) -> tarfile.TarInfo: + tar_info.uid = 0 + tar_info.gid = 0 + tar_info.uname = "root" + tar_info.gname = "root" + return tar_info + + +def render_template(relative_path: str, values: Dict[str, str]) -> str: + template = templates_root() / relative_path + return template.read_text(encoding="utf-8").format(**values) + + +def install_file(source: Path, destination: Path, mode: int) -> None: + if not source.exists(): + raise RuntimeError("Required packaging input is missing: {}".format(source)) + make_dir(destination.parent, 0o755) + shutil.copy2(source, destination) + destination.chmod(mode) + + +def write_text(path: Path, content: str, mode: int) -> None: + make_dir(path.parent, 0o755) + path.write_text(content, encoding="utf-8") + path.chmod(mode) + + +def make_dir(path: Path, mode: int) -> None: + path.mkdir(parents=True, exist_ok=True) + path.chmod(mode) + + +def normalize_directory_modes(root: Path) -> None: + root.chmod(0o755) + for path in root.rglob("*"): + if path.is_dir(): + path.chmod(0o755) + + +def reset_dir(path: Path) -> None: + if path.exists(): + shutil.rmtree(path) + path.mkdir(parents=True) + path.chmod(0o755) + + +def ensure_linux_host() -> None: + if platform.system() != "Linux": + raise RuntimeError("Linux installers can only be created on Linux") + + +def ensure_tools(tools: Iterable[str]) -> None: + missing = [tool for tool in tools if shutil.which(tool) is None] + if missing: + raise RuntimeError("Missing Linux packaging tools: {}".format(", ".join(missing))) + + +def run(command: List[str], error: str, cwd: Optional[Path] = None) -> None: + result = subprocess.run(command, cwd=cwd) + if result.returncode != 0: + raise RuntimeError(error) + + +def run_capture( + command: List[str], error: str, cwd: Optional[Path] = None +) -> subprocess.CompletedProcess: + result = subprocess.run( + command, + cwd=cwd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.stderr: + print(result.stderr, end="") + if result.returncode != 0: + raise RuntimeError("{}: {}".format(error, result.stderr.strip())) + return result + + +def templates_root() -> Path: + return Path(__file__).resolve().parent / "templates" diff --git a/development/packaging/linux/templates/debian/control.in b/development/packaging/linux/templates/debian/control.in new file mode 100644 index 000000000..4d6ffd450 --- /dev/null +++ b/development/packaging/linux/templates/debian/control.in @@ -0,0 +1,15 @@ +Package: chipmunk +Version: {version} +Section: devel +Priority: optional +Architecture: {architecture} +Maintainer: Alexandru Frincu +{depends_field}Installed-Size: {installed_size} +Homepage: https://github.com/esrlabs/chipmunk +Description: Fast Logfile Viewer for Analyzing Large Logfiles + Chipmunk is a fast logfile viewer designed for analyzing large logfiles. + It features super-fast search capabilities and is an invaluable tool for + developers who need to analyze log data. + . + This package provides a user-friendly interface for searching and analyzing + logfiles of any size. diff --git a/development/packaging/linux/templates/debian/copyright.in b/development/packaging/linux/templates/debian/copyright.in new file mode 100644 index 000000000..5ae38ef6c --- /dev/null +++ b/development/packaging/linux/templates/debian/copyright.in @@ -0,0 +1,22 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: chipmunk +Upstream-Contact: Dmitry Astafyev +Source: {source} + +Files: * +Copyright: 2023-2026, ESR Labs +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + On Debian systems, the complete text of the Apache License, Version 2 + can be found in "/usr/share/common-licenses/Apache-2.0". diff --git a/development/packaging/linux/templates/debian/shlibdeps-control.in b/development/packaging/linux/templates/debian/shlibdeps-control.in new file mode 100644 index 000000000..87cb84c2d --- /dev/null +++ b/development/packaging/linux/templates/debian/shlibdeps-control.in @@ -0,0 +1,12 @@ +Source: chipmunk +Section: devel +Priority: optional +Maintainer: Alexandru Frincu +Standards-Version: 4.6.2 +Homepage: https://github.com/esrlabs/chipmunk + +Package: chipmunk +Architecture: any +Depends: ${{shlibs:Depends}} +Description: Fast Logfile Viewer for Analyzing Large Logfiles + Chipmunk is a fast logfile viewer designed for analyzing large logfiles. diff --git a/development/packaging/linux/templates/rpm/chipmunk.spec.in b/development/packaging/linux/templates/rpm/chipmunk.spec.in new file mode 100644 index 000000000..e3c981647 --- /dev/null +++ b/development/packaging/linux/templates/rpm/chipmunk.spec.in @@ -0,0 +1,34 @@ +%global debug_package %{{nil}} + +Name: chipmunk +Version: {version} +Release: {release} +Summary: chipmunk is a fast logfile viewer that can deal with huge logfiles +License: Apache-2.0 +URL: https://github.com/esrlabs/chipmunk +Source0: {source} +Requires: hicolor-icon-theme + +%description +chipmunk is a fast logfile viewer that can deal with huge logfiles. It powers +a super fast search and is supposed to be a useful tool for developers who have +to analyze logfiles. + +%prep +%setup -q -c -T +tar -xzf %{{SOURCE0}} + +%build + +%install +rm -rf %{{buildroot}} +mkdir -p %{{buildroot}} +cp -a usr %{{buildroot}}/ + +%files +%defattr(-,root,root,-) +/usr/bin/chipmunk +/usr/share/applications/chipmunk.desktop +/usr/share/icons/hicolor/*/apps/chipmunk.png +%license /usr/share/licenses/chipmunk/LICENSE.txt +%doc /usr/share/doc/chipmunk/README.md diff --git a/development/scripts/release_app.py b/development/scripts/release_app.py index 295b4cea7..ba34ffd65 100755 --- a/development/scripts/release_app.py +++ b/development/scripts/release_app.py @@ -24,6 +24,12 @@ import tarfile from pathlib import Path +REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from development.packaging.linux import LinuxPackageConfig, package_linux_installers + try: import tomllib except ModuleNotFoundError: @@ -61,8 +67,9 @@ def main(): artifacts = [package_portable(version), package_windows_msi(version)] else: artifacts = [package_portable(version)] + artifacts.extend(package_linux_installers(linux_package_config(version))) - artifacts.append(package_cli_portable(cli_version_value)) + artifacts.append(package_cli_portable(cli_version_value, code_sign=args.code_sign)) for artifact in artifacts: print("Chipmunk release artifact created: {}".format(artifact)) @@ -121,13 +128,17 @@ def package_portable(version): return archive -def package_cli_portable(version): +def package_cli_portable(version, code_sign=False): """Create the portable CLI archive using the legacy flat layout.""" archive_root = "chipmunk-cli@{}-{}-portable".format(version, platform_name()) staging_dir = app_release_path() / archive_root reset_staging_dir(staging_dir) - shutil.copy2(cli_binary_path(), staging_dir / cli_binary_name()) + cli = staging_dir / cli_binary_name() + shutil.copy2(cli_binary_path(), cli) + + if is_macos(): + sign_and_notarize_macos_cli(cli, code_sign=code_sign) archive = app_release_path() / "{}.tgz".format(archive_root) write_flat_tgz_archive(staging_dir, archive) @@ -334,25 +345,34 @@ def write_windows_license_rtf(path): """Copy the license text shown by the Windows installer UI.""" shutil.copy2(windows_license_rtf_path(), path) + +def sign_macos_executable(path, error, entitlements=None): + """Sign a single macOS executable with the Developer ID Application identity.""" + signing_id = require_env("SIGNING_ID") + cmd = [ + "codesign", + "--force", + "--sign", + signing_id, + "--timestamp", + "--options", + "runtime", + ] + if entitlements is not None: + cmd.extend(["--entitlements", str(entitlements)]) + cmd.append(str(path)) + run(cmd, error=error) + + def sign_app(app_root): """Sign the app bundle with the Developer ID Application identity.""" signing_id = require_env("SIGNING_ID") executable = app_root / "Contents" / "MacOS" / "chipmunk" - run( - [ - "codesign", - "--force", - "--sign", - signing_id, - "--timestamp", - "--options", - "runtime", - "--entitlements", - str(entitlements_path()), - str(executable), - ], + sign_macos_executable( + executable, error="Signing chipmunk executable failed", + entitlements=entitlements_path(), ) run( [ @@ -377,6 +397,34 @@ def sign_app(app_root): ) +def sign_and_notarize_macos_cli(executable, code_sign): + """Sign and notarize the staged macOS CLI binary before archiving it.""" + if not code_sign: + return + if not signing_allowed(): + print( + "Skipping macOS CLI code signing because required environment " + "variables are missing or SKIP_NOTARIZE is set.", + file=sys.stderr, + ) + return + + sign_macos_executable(executable, error="Signing chipmunk CLI failed") + run( + ["codesign", "--verify", "--verbose=4", str(executable)], + error="Verifying chipmunk CLI signature failed", + ) + + notarization_dir = app_release_path() / "cli-notarization" + notarization_archive = notarization_dir / "chipmunk-cli-notarization.zip" + notarization_dir.mkdir(parents=True, exist_ok=True) + try: + zip_macos_path(executable, notarization_archive) + notarize_archive(notarization_archive) + finally: + shutil.rmtree(notarization_dir, ignore_errors=True) + + def notarize_archive(archive): """Submit a signed macOS archive to Apple and wait for acceptance.""" result = subprocess.run( @@ -410,8 +458,13 @@ def notarize_archive(archive): def zip_macos_bundle(app_root, archive): """Create the zip format Apple expects for app bundle notarization.""" + zip_macos_path(app_root, archive) + + +def zip_macos_path(source_path, archive): + """Create the zip format Apple expects for notarization uploads.""" run( - ["ditto", "-c", "-k", "--keepParent", str(app_root), str(archive)], + ["ditto", "-c", "-k", "--keepParent", str(source_path), str(archive)], error="Creating macOS chipmunk zip archive failed", ) @@ -618,7 +671,7 @@ def sign_windows_file(path): def repo_root(): - return Path(__file__).resolve().parents[2] + return REPO_ROOT def app_root(): @@ -695,6 +748,18 @@ def macos_pkg_postinstall_path(): return app_root() / "data" / "mac" / "pkg-scripts" / "postinstall" +def linux_package_config(version): + return LinuxPackageConfig( + version=version, + dist_dir=app_release_path(), + app_binary=app_binary_path(), + desktop_file=app_root() / "data" / "linux" / "chipmunk.desktop", + icon_dir=app_root() / "data" / "icons" / "png", + readme=repo_readme_path(), + license_file=repo_root() / "LICENSE.txt", + ) + + def platform_name(): """Return the platform token used in public release artifact names.""" system = platform.system().lower()