diff --git a/.github/workflows/fork-ci.yml b/.github/workflows/fork-ci.yml new file mode 100644 index 000000000000..d7a0ef629b1b --- /dev/null +++ b/.github/workflows/fork-ci.yml @@ -0,0 +1,88 @@ +name: Fork CI + +on: + push: + branches: [main, 'sid/**'] + pull_request: + branches: [main] + +concurrency: + group: fork-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-macos: + name: Build macOS (LAB) + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.15.2 + + - name: Cache GhosttyKit + id: cache-ghosttykit + uses: actions/cache@v4 + with: + path: ghostty/zig-out/lib/GhosttyKit.xcframework + key: ghosttykit-${{ hashFiles('ghostty/.git/HEAD', 'ghostty/.git/refs/heads/**') }} + + - name: Build GhosttyKit + if: steps.cache-ghosttykit.outputs.cache-hit != 'true' + run: | + cd ghostty + zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast + + - name: Build cmux LAB (Debug) + run: | + xcodebuild -project GhosttyTabs.xcodeproj \ + -scheme cmux \ + -configuration Debug \ + -destination 'platform=macOS' \ + -derivedDataPath /tmp/cmux-fork-ci \ + ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Fork \ + PRODUCT_NAME="cmux LAB" \ + build + + - name: Build cmux LAB (Release) + run: | + xcodebuild -project GhosttyTabs.xcodeproj \ + -scheme cmux \ + -configuration Release \ + -destination 'platform=macOS' \ + -derivedDataPath build \ + ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Fork \ + PRODUCT_NAME="cmux LAB" \ + build + + build-linux-daemon: + name: Build Linux daemon + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.15.2 + + - name: Build cmuxd (x86_64) + run: | + cd cmuxd + zig build -Doptimize=ReleaseFast + + nix-check: + name: Nix flake check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/magic-nix-cache-action@main + - run: nix flake check + - run: nix develop --command echo "dev shell OK" diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml new file mode 100644 index 000000000000..375edb7c98e4 --- /dev/null +++ b/.github/workflows/fork-release.yml @@ -0,0 +1,196 @@ +name: Fork Release (LAB) + +on: + push: + tags: + - 'lab-v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag (e.g. lab-v0.62.1)' + required: true + +concurrency: + group: fork-release + cancel-in-progress: false + +jobs: + build-macos: + name: Build macOS .app + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.15.2 + + - name: Build GhosttyKit + run: | + cd ghostty + zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast + + - name: Build cmux LAB (Release) + run: | + xcodebuild -project GhosttyTabs.xcodeproj \ + -scheme cmux \ + -configuration Release \ + -destination 'platform=macOS' \ + -derivedDataPath build \ + ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Fork \ + PRODUCT_NAME="cmux LAB" \ + PRODUCT_BUNDLE_IDENTIFIER=com.cmuxterm.app.lab \ + build + + - name: Package DMG + run: | + APP_PATH="build/Build/Products/Release/cmux LAB.app" + npx create-dmg@8.0.0 "$APP_PATH" . --overwrite || true + mv *.dmg cmux-lab-macos.dmg 2>/dev/null || mv "cmux LAB"*.dmg cmux-lab-macos.dmg + + - name: Upload macOS artifact + uses: actions/upload-artifact@v4 + with: + name: cmux-lab-macos + path: cmux-lab-macos.dmg + + build-linux-daemon: + name: Build Linux daemon (cmuxd) + runs-on: ubuntu-latest + strategy: + matrix: + include: + - target: x86_64-linux + arch: amd64 + - target: aarch64-linux + arch: arm64 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.15.2 + + - name: Build cmuxd + run: | + cd cmuxd + zig build -Doptimize=ReleaseFast -Dtarget=${{ matrix.target }}-gnu + cp zig-out/bin/cmuxd ../cmuxd-${{ matrix.arch }} + + - name: Create DEB package + run: | + ARCH=${{ matrix.arch }} + VERSION=${GITHUB_REF_NAME#lab-v} + [ -z "$VERSION" ] && VERSION="0.0.0" + PKG_DIR="cmux-lab_${VERSION}_${ARCH}" + mkdir -p "${PKG_DIR}/DEBIAN" + mkdir -p "${PKG_DIR}/usr/bin" + mkdir -p "${PKG_DIR}/usr/share/doc/cmux-lab" + + cp cmuxd-${ARCH} "${PKG_DIR}/usr/bin/cmuxd-lab" + chmod 755 "${PKG_DIR}/usr/bin/cmuxd-lab" + + cat > "${PKG_DIR}/DEBIAN/control" < + Description: cmux LAB remote daemon with FIDO2/WebAuthn support + Terminal multiplexer remote daemon for enterprise auth testing. + Homepage: https://github.com/Jesssullivan/cmux + CTRL + # Remove leading spaces from heredoc + sed -i 's/^ //' "${PKG_DIR}/DEBIAN/control" + + dpkg-deb --build "${PKG_DIR}" + mv "${PKG_DIR}.deb" "cmux-lab_${VERSION}_${ARCH}.deb" + + - name: Create RPM package + if: matrix.arch == 'amd64' + run: | + sudo apt-get install -y rpm + VERSION=${GITHUB_REF_NAME#lab-v} + [ -z "$VERSION" ] && VERSION="0.0.0" + + mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} + cp cmuxd-amd64 rpmbuild/SOURCES/cmuxd-lab + + cat > rpmbuild/SPECS/cmux-lab.spec <<'SPEC' + Name: cmux-lab + Version: %{version} + Release: 1%{?dist} + Summary: cmux LAB remote daemon with FIDO2/WebAuthn support + License: MIT + URL: https://github.com/Jesssullivan/cmux + + %description + Terminal multiplexer remote daemon for enterprise auth testing. + + %install + mkdir -p %{buildroot}/usr/bin + cp %{_sourcedir}/cmuxd-lab %{buildroot}/usr/bin/cmuxd-lab + chmod 755 %{buildroot}/usr/bin/cmuxd-lab + + %files + /usr/bin/cmuxd-lab + SPEC + sed -i 's/^ //' rpmbuild/SPECS/cmux-lab.spec + sed -i "s/%{version}/${VERSION}/" rpmbuild/SPECS/cmux-lab.spec + + rpmbuild --define "_topdir $(pwd)/rpmbuild" -bb rpmbuild/SPECS/cmux-lab.spec || true + + - name: Upload Linux artifacts + uses: actions/upload-artifact@v4 + with: + name: cmux-lab-linux-${{ matrix.arch }} + path: | + cmuxd-${{ matrix.arch }} + cmux-lab_*.deb + rpmbuild/RPMS/**/*.rpm + + release: + name: Create GitHub Release + needs: [build-macos, build-linux-daemon] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Create release + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="${GITHUB_REF_NAME}" + [ -z "$TAG" ] && TAG="${{ github.event.inputs.tag }}" + + # Collect all release assets + ASSETS=() + for f in cmux-lab-macos/*.dmg cmux-lab-linux-*/*.deb cmux-lab-linux-*/cmuxd-* cmux-lab-linux-*/rpmbuild/RPMS/**/*.rpm; do + [ -f "$f" ] && ASSETS+=("$f") + done + + gh release create "$TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --title "cmux LAB $TAG" \ + --notes "Fork release with FIDO2/WebAuthn enterprise auth support. + + ## Assets + - **cmux-lab-macos.dmg** — macOS .app bundle (trans-themed LAB branding) + - **cmuxd-amd64** / **cmuxd-arm64** — Linux remote daemon binaries + - **.deb** — Debian/Ubuntu packages + - **.rpm** — RHEL/Fedora packages (amd64) + + ## Nix + \`\`\`bash + nix run github:Jesssullivan/cmux/$TAG + \`\`\` + " \ + "${ASSETS[@]}" diff --git a/Assets.xcassets/AppIcon-Fork.appiconset/128.png b/Assets.xcassets/AppIcon-Fork.appiconset/128.png new file mode 100644 index 000000000000..38f939ef674f Binary files /dev/null and b/Assets.xcassets/AppIcon-Fork.appiconset/128.png differ diff --git a/Assets.xcassets/AppIcon-Fork.appiconset/128@2x.png b/Assets.xcassets/AppIcon-Fork.appiconset/128@2x.png new file mode 100644 index 000000000000..36a13fe388ac Binary files /dev/null and b/Assets.xcassets/AppIcon-Fork.appiconset/128@2x.png differ diff --git a/Assets.xcassets/AppIcon-Fork.appiconset/16.png b/Assets.xcassets/AppIcon-Fork.appiconset/16.png new file mode 100644 index 000000000000..b1d60bf5b829 Binary files /dev/null and b/Assets.xcassets/AppIcon-Fork.appiconset/16.png differ diff --git a/Assets.xcassets/AppIcon-Fork.appiconset/16@2x.png b/Assets.xcassets/AppIcon-Fork.appiconset/16@2x.png new file mode 100644 index 000000000000..e123506e6192 Binary files /dev/null and b/Assets.xcassets/AppIcon-Fork.appiconset/16@2x.png differ diff --git a/Assets.xcassets/AppIcon-Fork.appiconset/256.png b/Assets.xcassets/AppIcon-Fork.appiconset/256.png new file mode 100644 index 000000000000..36a13fe388ac Binary files /dev/null and b/Assets.xcassets/AppIcon-Fork.appiconset/256.png differ diff --git a/Assets.xcassets/AppIcon-Fork.appiconset/256@2x.png b/Assets.xcassets/AppIcon-Fork.appiconset/256@2x.png new file mode 100644 index 000000000000..508a9397cebf Binary files /dev/null and b/Assets.xcassets/AppIcon-Fork.appiconset/256@2x.png differ diff --git a/Assets.xcassets/AppIcon-Fork.appiconset/32.png b/Assets.xcassets/AppIcon-Fork.appiconset/32.png new file mode 100644 index 000000000000..e123506e6192 Binary files /dev/null and b/Assets.xcassets/AppIcon-Fork.appiconset/32.png differ diff --git a/Assets.xcassets/AppIcon-Fork.appiconset/32@2x.png b/Assets.xcassets/AppIcon-Fork.appiconset/32@2x.png new file mode 100644 index 000000000000..9ffae0950a43 Binary files /dev/null and b/Assets.xcassets/AppIcon-Fork.appiconset/32@2x.png differ diff --git a/Assets.xcassets/AppIcon-Fork.appiconset/512.png b/Assets.xcassets/AppIcon-Fork.appiconset/512.png new file mode 100644 index 000000000000..508a9397cebf Binary files /dev/null and b/Assets.xcassets/AppIcon-Fork.appiconset/512.png differ diff --git a/Assets.xcassets/AppIcon-Fork.appiconset/512@2x.png b/Assets.xcassets/AppIcon-Fork.appiconset/512@2x.png new file mode 100644 index 000000000000..fa30dd32fcc8 Binary files /dev/null and b/Assets.xcassets/AppIcon-Fork.appiconset/512@2x.png differ diff --git a/Assets.xcassets/AppIcon-Fork.appiconset/Contents.json b/Assets.xcassets/AppIcon-Fork.appiconset/Contents.json new file mode 100644 index 000000000000..f6fd6f1fa0d3 --- /dev/null +++ b/Assets.xcassets/AppIcon-Fork.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images": [ + { + "filename": "16.png", + "idiom": "mac", + "scale": "1x", + "size": "16x16" + }, + { + "filename": "16@2x.png", + "idiom": "mac", + "scale": "2x", + "size": "16x16" + }, + { + "filename": "32.png", + "idiom": "mac", + "scale": "1x", + "size": "32x32" + }, + { + "filename": "32@2x.png", + "idiom": "mac", + "scale": "2x", + "size": "32x32" + }, + { + "filename": "128.png", + "idiom": "mac", + "scale": "1x", + "size": "128x128" + }, + { + "filename": "128@2x.png", + "idiom": "mac", + "scale": "2x", + "size": "128x128" + }, + { + "filename": "256.png", + "idiom": "mac", + "scale": "1x", + "size": "256x256" + }, + { + "filename": "256@2x.png", + "idiom": "mac", + "scale": "2x", + "size": "256x256" + }, + { + "filename": "512.png", + "idiom": "mac", + "scale": "1x", + "size": "512x512" + }, + { + "filename": "512@2x.png", + "idiom": "mac", + "scale": "2x", + "size": "512x512" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000000..f23fa6359fbc --- /dev/null +++ b/flake.nix @@ -0,0 +1,107 @@ +{ + description = "cmux LAB - fork with FIDO2/WebAuthn support for enterprise auth testing"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + version = "0.62.1-lab"; + in + { + packages = { + # Darwin .app bundle (macOS only) + cmux-darwin = pkgs.stdenv.mkDerivation { + pname = "cmux-lab"; + inherit version; + src = ./.; + + # The macOS .app is built via xcodebuild externally; + # this derivation packages a pre-built .app from CI artifacts. + # For local dev, use ./scripts/reload.sh --tag lab + phases = [ "installPhase" ]; + + installPhase = '' + mkdir -p $out/Applications + if [ -d "$src/build/Build/Products/Release/cmux.app" ]; then + cp -R "$src/build/Build/Products/Release/cmux.app" "$out/Applications/cmux LAB.app" + else + echo "No pre-built .app found. Build with xcodebuild first." + exit 1 + fi + + # Install CLI wrapper + mkdir -p $out/bin + if [ -f "$out/Applications/cmux LAB.app/Contents/Resources/bin/cmux" ]; then + ln -s "$out/Applications/cmux LAB.app/Contents/Resources/bin/cmux" $out/bin/cmux-lab + fi + ''; + + meta = with pkgs.lib; { + description = "cmux LAB - terminal multiplexer with FIDO2/WebAuthn browser support"; + homepage = "https://github.com/Jesssullivan/cmux"; + license = licenses.mit; + platforms = platforms.darwin; + mainProgram = "cmux-lab"; + }; + }; + + # RPM package spec (Linux) + cmux-rpm = pkgs.stdenv.mkDerivation { + pname = "cmux-lab"; + inherit version; + src = ./.; + + nativeBuildInputs = with pkgs; [ rpm ]; + + buildPhase = '' + # Build cmuxd for Linux + if command -v zig &>/dev/null && [ -d cmuxd ]; then + cd cmuxd && zig build -Doptimize=ReleaseFast && cd .. + fi + ''; + + installPhase = '' + mkdir -p $out/bin $out/share/cmux-lab + if [ -f cmuxd/zig-out/bin/cmuxd ]; then + cp cmuxd/zig-out/bin/cmuxd $out/bin/cmuxd-lab + fi + cp -r scripts $out/share/cmux-lab/ 2>/dev/null || true + ''; + + meta = with pkgs.lib; { + description = "cmux LAB remote daemon for Linux"; + homepage = "https://github.com/Jesssullivan/cmux"; + license = licenses.mit; + platforms = platforms.linux; + }; + }; + + default = if pkgs.stdenv.isDarwin + then self.packages.${system}.cmux-darwin + else self.packages.${system}.cmux-rpm; + }; + + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + zig_0_14 + python3 + python3Packages.pillow + ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ + rpm + dpkg + ]; + + shellHook = '' + echo "cmux LAB dev shell" + echo " macOS: ./scripts/reload.sh --tag lab" + echo " Icons: nix-shell -p python3Packages.pillow --run 'python3 scripts/generate_fork_icon.py'" + ''; + }; + } + ); +} diff --git a/scripts/generate_fork_icon.py b/scripts/generate_fork_icon.py new file mode 100644 index 000000000000..d9abd8388c7f --- /dev/null +++ b/scripts/generate_fork_icon.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +"""Generate fork-specific app icon with trans flag themed banner. + +Takes the AppIcon-Debug icons (which have an orange "DEV" banner) and: +1. Recolors the banner to trans flag gradient (light blue → pink → white → pink → blue) +2. Replaces the "DEV" text with "LAB" + +Trans flag colors: + Light blue: #5BCEFA (91, 206, 250) + Pink: #F5A9B8 (245, 169, 184) + White: #FFFFFF (255, 255, 255) + +This creates a visually distinct build from upstream cmux for lab testing. +""" +import os +import json +from PIL import Image, ImageDraw, ImageFont + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC_DIR = os.path.join(REPO, "Assets.xcassets", "AppIcon-Debug.appiconset") +DST_DIR = os.path.join(REPO, "Assets.xcassets", "AppIcon-Fork.appiconset") + +# Trans flag colors +TRANS_BLUE = (91, 206, 250) +TRANS_PINK = (245, 169, 184) +TRANS_WHITE = (255, 255, 255) + +SIZES = [ + ("16.png", 16), + ("16@2x.png", 32), + ("32.png", 32), + ("32@2x.png", 64), + ("128.png", 128), + ("128@2x.png", 256), + ("256.png", 256), + ("256@2x.png", 512), + ("512.png", 512), + ("512@2x.png", 1024), +] + + +def lerp_color(c1, c2, t): + """Linearly interpolate between two RGB tuples.""" + return tuple(int(a + (b - a) * t) for a, b in zip(c1, c2)) + + +def trans_gradient_color(y_frac): + """Return the trans flag color at a given vertical fraction (0=top, 1=bottom). + + The flag has 5 horizontal stripes: + 0.0 - 0.2: light blue + 0.2 - 0.4: pink + 0.4 - 0.6: white + 0.6 - 0.8: pink + 0.8 - 1.0: light blue + We use smooth blending between stripes. + """ + if y_frac <= 0.1: + return TRANS_BLUE + elif y_frac <= 0.2: + return lerp_color(TRANS_BLUE, TRANS_PINK, (y_frac - 0.1) / 0.1) + elif y_frac <= 0.3: + return TRANS_PINK + elif y_frac <= 0.4: + return lerp_color(TRANS_PINK, TRANS_WHITE, (y_frac - 0.3) / 0.1) + elif y_frac <= 0.6: + return TRANS_WHITE + elif y_frac <= 0.7: + return lerp_color(TRANS_WHITE, TRANS_PINK, (y_frac - 0.6) / 0.1) + elif y_frac <= 0.8: + return TRANS_PINK + elif y_frac <= 0.9: + return lerp_color(TRANS_PINK, TRANS_BLUE, (y_frac - 0.8) / 0.1) + else: + return TRANS_BLUE + + +def recolor_banner(img: Image.Image) -> Image.Image: + """Recolor the orange banner to trans gradient and replace DEV with LAB.""" + img = img.convert("RGBA") + w, h = img.size + pixels = img.load() + + # The banner occupies roughly the bottom 18% of the icon. + banner_y = int(h * 0.82) + banner_h = h - banner_y + + # Pass 1: Recolor orange pixels to trans gradient. + for y in range(h): + for x in range(w): + r, g, b, a = pixels[x, y] + if a == 0: + continue + # Detect orange: high R, moderate G, very low B + if r > 180 and g < 180 and b < 100 and r > g and r - b > 100: + orange_strength = min(r / 255.0, 1.0) + # Map y position within banner to trans gradient + if y >= banner_y: + frac = (y - banner_y) / max(banner_h, 1) + else: + frac = 0.0 + tc = trans_gradient_color(frac) + nr = int(tc[0] * orange_strength) + ng = int(tc[1] * orange_strength) + nb = int(tc[2] * orange_strength) + pixels[x, y] = (nr, ng, nb, a) + + # Pass 2: Replace the "DEV" text with "LAB". + text_pixels = [] + for y in range(banner_y, h): + for x in range(w): + r, g, b, a = pixels[x, y] + if r > 220 and g > 220 and b > 220 and a > 200: + text_pixels.append((x, y)) + + if text_pixels: + min_x = min(p[0] for p in text_pixels) + max_x = max(p[0] for p in text_pixels) + min_y = min(p[1] for p in text_pixels) + max_y = max(p[1] for p in text_pixels) + + pad = max(2, int(h * 0.005)) + min_x = max(0, min_x - pad) + max_x = min(w - 1, max_x + pad) + min_y = max(banner_y, min_y - pad) + max_y = min(h - 1, max_y + pad) + + # Fill text area with gradient + draw = ImageDraw.Draw(img) + for y in range(min_y, max_y + 1): + frac = (y - banner_y) / max(banner_h, 1) + c = trans_gradient_color(frac) + draw.line([(min_x, y), (max_x, y)], fill=(*c, 255)) + + # Draw "LAB" centered in the banner + text = "LAB" + text_area_h = max_y - min_y + font_size = max(int(text_area_h * 0.85), 6) + + font = None + for font_path in [ + "/System/Library/Fonts/SFCompact-Bold.otf", + "/System/Library/Fonts/Supplemental/Arial Bold.ttf", + "/System/Library/Fonts/Helvetica.ttc", + ]: + if os.path.exists(font_path): + try: + font = ImageFont.truetype(font_path, font_size) + break + except Exception: + continue + if font is None: + font = ImageFont.load_default() + + bbox = draw.textbbox((0, 0), text, font=font) + tw = bbox[2] - bbox[0] + th = bbox[3] - bbox[1] + tx = (w - tw) // 2 + ty = banner_y + (banner_h - th) // 2 - bbox[1] + + # Dark text for readability against the light trans gradient + draw.text((tx, ty), text, fill=(40, 40, 60, 255), font=font) + + return img + + +def main(): + os.makedirs(DST_DIR, exist_ok=True) + + for filename, pixel_size in SIZES: + src_path = os.path.join(SRC_DIR, filename) + dst_path = os.path.join(DST_DIR, filename) + + if not os.path.exists(src_path): + print(f" SKIP {filename} (source not found)") + continue + + img = Image.open(src_path) + if img.size != (pixel_size, pixel_size): + img = img.resize((pixel_size, pixel_size), Image.LANCZOS) + + result = recolor_banner(img) + result.save(dst_path, "PNG") + print(f" {filename} ({pixel_size}x{pixel_size})") + + # Write Contents.json + contents = { + "images": [ + {"filename": f, "idiom": "mac", "scale": s, "size": sz} + for f, s, sz in [ + ("16.png", "1x", "16x16"), + ("16@2x.png", "2x", "16x16"), + ("32.png", "1x", "32x32"), + ("32@2x.png", "2x", "32x32"), + ("128.png", "1x", "128x128"), + ("128@2x.png", "2x", "128x128"), + ("256.png", "1x", "256x256"), + ("256@2x.png", "2x", "256x256"), + ("512.png", "1x", "512x512"), + ("512@2x.png", "2x", "512x512"), + ] + ], + "info": {"author": "xcode", "version": 1}, + } + with open(os.path.join(DST_DIR, "Contents.json"), "w") as f: + json.dump(contents, f, indent=2) + + print(f"\nGenerated {len(SIZES)} icons in {DST_DIR}") + + +if __name__ == "__main__": + main()