- install.py / uninstall.py: full Python installer for Linux, macOS, Windows
- detect_platform.py: shared Inkscape config path detection
- Windows config path: %APPDATA%\inkscape (native/winget/scoop) or
%LOCALAPPDATA%\Packages\org.inkscape.Inkscape* (MS Store)
- Windows desktop: Start Menu .lnk via PowerShell WScript.Shell COM object
- macOS: XDG / ~/Library/Application Support detection, no desktop step
- install.sh / uninstall.sh: reduced to one-line exec python3 wrappers
- eliminates SC1091 (source not followed) and SC2155 (declare+assign)
- scripts/detect_platform.sh: export INKSCAPE_CONFIG / INKSCAPE_INSTALL_METHOD
- fixes SC2034 (variables appear unused to shellcheck)
- assets/illuscape.ico: pre-rendered 16/32/48/64/128/256px ICO (ImageMagick)
- tests/test_install.py: pytest integration tests replacing test_install.bats
- cross-platform fake inkscape injected via PATH
- 12 tests covering backup, palettes, templates, prefs, uninstall restore
- ci.yml: add windows-latest matrix leg; shellcheck/inkscape Linux-only;
bats removed in favour of pytest integration tests; 27 tests total
288 lines
11 KiB
Python
288 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Illuscape installer — cross-platform (Linux, Windows, macOS).
|
|
|
|
Usage:
|
|
python3 install.py [--preset=cc|cs6] [--yes]
|
|
./install.sh [--preset=cc|cs6] [--yes] (Linux / macOS)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import datetime
|
|
import os
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Ensure scripts/ is importable regardless of cwd
|
|
SCRIPT_DIR = Path(__file__).parent
|
|
sys.path.insert(0, str(SCRIPT_DIR / "scripts"))
|
|
|
|
from detect_platform import detect_config # noqa: E402
|
|
|
|
ICON_SIZES = (16, 32, 48, 64, 128, 256, 512)
|
|
|
|
|
|
# ── Dependency check ──────────────────────────────────────────────────────────
|
|
|
|
def check_deps(auto_yes: bool) -> None:
|
|
"""Verify Inkscape is present and meets the minimum version."""
|
|
if not shutil.which("inkscape"):
|
|
sys.exit("✗ Inkscape is not installed. Install Inkscape first, then run Illuscape.")
|
|
|
|
try:
|
|
raw = subprocess.run(
|
|
["inkscape", "--version"],
|
|
capture_output=True, text=True, timeout=10,
|
|
).stdout
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return # cannot check version — proceed anyway
|
|
|
|
match = re.search(r"(\d+)\.(\d+)", raw)
|
|
if match:
|
|
major, minor = int(match.group(1)), int(match.group(2))
|
|
if major < 1 or (major == 1 and minor < 2):
|
|
version = f"{major}.{minor}"
|
|
print(f"⚠ Inkscape {version} detected. Illuscape targets Inkscape 1.2+.")
|
|
print(" Some settings may not apply correctly. Continue anyway? [y/N]")
|
|
if not auto_yes and input().strip().lower() != "y":
|
|
sys.exit(0)
|
|
|
|
|
|
# ── Backup ────────────────────────────────────────────────────────────────────
|
|
|
|
def backup_config(config_path: Path) -> None:
|
|
"""Copy the config directory to a timestamped backup; idempotent on repeat runs."""
|
|
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
backup_dir = Path(str(config_path) + f".bak-illuscape-{timestamp}")
|
|
|
|
existing = sorted(config_path.parent.glob(config_path.name + ".bak-illuscape-*"))
|
|
if existing:
|
|
print("→ Backup already exists — skipping (idempotent)")
|
|
return
|
|
|
|
if config_path.is_dir():
|
|
shutil.copytree(config_path, backup_dir)
|
|
print(f"→ Backed up to: {backup_dir}")
|
|
else:
|
|
print("→ No existing config to back up (fresh install)")
|
|
|
|
|
|
# ── Preset selection ──────────────────────────────────────────────────────────
|
|
|
|
def choose_preset(preset: str, auto_yes: bool) -> str:
|
|
"""Prompt the user for CC / CS6 unless preset is already provided."""
|
|
if preset:
|
|
return preset
|
|
|
|
print()
|
|
print("Which Illustrator era are you coming from?")
|
|
print(" [1] CC — current Creative Cloud (default)")
|
|
print(" [2] CS6 — last perpetual license")
|
|
print("Choice [1]: ", end="", flush=True)
|
|
if auto_yes:
|
|
print("1 (auto)")
|
|
return "cc"
|
|
choice = input().strip()
|
|
return "cs6" if choice == "2" else "cc"
|
|
|
|
|
|
# ── Preferences merge ─────────────────────────────────────────────────────────
|
|
|
|
def merge_prefs(config_path: Path, preset: str) -> None:
|
|
"""Invoke merge_prefs.py to upsert the preferences patch into Inkscape's prefs."""
|
|
subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT_DIR / "scripts/merge_prefs.py"),
|
|
"--prefs", str(config_path / "preferences.xml"),
|
|
"--patch", str(SCRIPT_DIR / "config/preferences-patch.xml"),
|
|
"--preset", preset,
|
|
],
|
|
check=True,
|
|
)
|
|
|
|
|
|
# ── Config file copy ──────────────────────────────────────────────────────────
|
|
|
|
def copy_files(config_path: Path, preset: str) -> None:
|
|
"""Copy keys, palettes, templates, and symbols into the Inkscape config dir."""
|
|
for subdir in ("keys", "palettes", "templates", "symbols", "splashscreens"):
|
|
(config_path / subdir).mkdir(parents=True, exist_ok=True)
|
|
|
|
shutil.copy2(
|
|
SCRIPT_DIR / f"config/keys/illustrator-{preset}.xml",
|
|
config_path / "keys/",
|
|
)
|
|
for palette in (SCRIPT_DIR / "config/palettes").glob("*.gpl"):
|
|
shutil.copy2(palette, config_path / "palettes/")
|
|
for template in (SCRIPT_DIR / "config/templates").glob("*.svg"):
|
|
shutil.copy2(template, config_path / "templates/")
|
|
for symbol in (SCRIPT_DIR / "config/symbols").glob("*.svg"):
|
|
shutil.copy2(symbol, config_path / "symbols/")
|
|
|
|
print("→ Config files copied")
|
|
|
|
|
|
# ── Desktop integration ───────────────────────────────────────────────────────
|
|
|
|
def install_desktop(config_path: Path, install_method: str) -> None:
|
|
"""Install a desktop launcher / Start Menu shortcut for the current platform."""
|
|
system = platform.system()
|
|
if system == "Linux" and install_method == "native":
|
|
_install_linux_desktop(config_path)
|
|
elif system == "Windows":
|
|
_install_windows_desktop(config_path)
|
|
# macOS: Inkscape.app handles its own dock icon; no extra step needed.
|
|
|
|
|
|
def _install_linux_desktop(config_path: Path) -> None:
|
|
app_dir = Path.home() / ".local/share/applications"
|
|
icon_src = SCRIPT_DIR / "assets/illuscape.svg"
|
|
splash_src = SCRIPT_DIR / "assets/splash.svg"
|
|
|
|
app_dir.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(SCRIPT_DIR / "assets/org.inkscape.Inkscape.desktop", app_dir)
|
|
|
|
for size in ICON_SIZES:
|
|
icon_dir = Path.home() / f".local/share/icons/hicolor/{size}x{size}/apps"
|
|
icon_dir.mkdir(parents=True, exist_ok=True)
|
|
icon_out = icon_dir / "illuscape.png"
|
|
if _rsvg_convert(icon_src, icon_out, size, size):
|
|
continue
|
|
if _inkscape_export(icon_src, icon_out, size, size):
|
|
continue
|
|
print(f"⚠ Could not rasterize icon at {size}px (install rsvg-convert for icons)")
|
|
|
|
splash_out = config_path / "splashscreens/illuscape-splash.png"
|
|
_rsvg_convert(splash_src, splash_out, 600, 400)
|
|
|
|
subprocess.run(
|
|
["update-desktop-database", str(app_dir)],
|
|
capture_output=True,
|
|
)
|
|
print("→ Desktop launcher installed")
|
|
|
|
|
|
def _install_windows_desktop(config_path: Path) -> None:
|
|
"""
|
|
Create a Start Menu .lnk shortcut via PowerShell WScript.Shell COM object.
|
|
The ICO is copied to the config dir so it stays with the config.
|
|
"""
|
|
ico_src = SCRIPT_DIR / "assets/illuscape.ico"
|
|
ico_dst = config_path / "illuscape.ico"
|
|
|
|
if ico_src.exists():
|
|
shutil.copy2(ico_src, ico_dst)
|
|
|
|
start_menu = (
|
|
Path(os.environ.get("APPDATA", ""))
|
|
/ "Microsoft/Windows/Start Menu/Programs"
|
|
)
|
|
lnk_path = start_menu / "Illuscape.lnk"
|
|
inkscape_exe = shutil.which("inkscape") or "inkscape.exe"
|
|
|
|
# Build a PowerShell one-liner using WScript.Shell
|
|
ps_lines = [
|
|
"$ws = New-Object -ComObject WScript.Shell",
|
|
f"$s = $ws.CreateShortcut('{lnk_path}')",
|
|
f"$s.TargetPath = '{inkscape_exe}'",
|
|
"$s.Description = 'Inkscape with Illustrator-style config'",
|
|
]
|
|
if ico_dst.exists():
|
|
ps_lines.append(f"$s.IconLocation = '{ico_dst}'")
|
|
ps_lines.append("$s.Save()")
|
|
|
|
try:
|
|
subprocess.run(
|
|
["powershell", "-NoProfile", "-Command", "; ".join(ps_lines)],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
print("→ Start Menu shortcut installed")
|
|
except (FileNotFoundError, subprocess.CalledProcessError) as exc:
|
|
print(f"⚠ Could not create Start Menu shortcut: {exc}")
|
|
print(" You can create one manually pointing to inkscape.exe.")
|
|
|
|
|
|
# ── Icon rasterization helpers ────────────────────────────────────────────────
|
|
|
|
def _rsvg_convert(src: Path, dst: Path, w: int, h: int) -> bool:
|
|
"""Rasterize *src* SVG to *dst* PNG using rsvg-convert. Returns True on success."""
|
|
if not shutil.which("rsvg-convert"):
|
|
return False
|
|
result = subprocess.run(
|
|
["rsvg-convert", "-w", str(w), "-h", str(h), str(src), "-o", str(dst)],
|
|
capture_output=True,
|
|
)
|
|
return result.returncode == 0
|
|
|
|
|
|
def _inkscape_export(src: Path, dst: Path, w: int, h: int) -> bool:
|
|
"""
|
|
Rasterize *src* SVG using Inkscape 1.x export flags.
|
|
(--export-png was removed in Inkscape 1.0)
|
|
"""
|
|
result = subprocess.run(
|
|
[
|
|
"inkscape",
|
|
f"--export-filename={dst}",
|
|
"--export-type=png",
|
|
f"--export-width={w}",
|
|
f"--export-height={h}",
|
|
str(src),
|
|
],
|
|
capture_output=True,
|
|
)
|
|
return result.returncode == 0
|
|
|
|
|
|
# ── CLI ───────────────────────────────────────────────────────────────────────
|
|
|
|
def _parse_args() -> argparse.Namespace:
|
|
p = argparse.ArgumentParser(description="Illuscape — Inkscape with Illustrator feel")
|
|
p.add_argument(
|
|
"--preset",
|
|
choices=["cc", "cs6"],
|
|
default="",
|
|
help="Illustrator shortcut era: cc (default) or cs6",
|
|
)
|
|
p.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompts")
|
|
return p.parse_args()
|
|
|
|
|
|
def main() -> None:
|
|
args = _parse_args()
|
|
|
|
print("╔══════════════════════════════╗")
|
|
print("║ Illuscape Installer ║")
|
|
print("╚══════════════════════════════╝")
|
|
print()
|
|
|
|
check_deps(args.yes)
|
|
config_path, install_method = detect_config()
|
|
print(f"→ Inkscape config: {config_path} ({install_method})")
|
|
|
|
backup_config(config_path)
|
|
preset = choose_preset(args.preset, args.yes)
|
|
print(f"→ Using preset: {preset}")
|
|
|
|
merge_prefs(config_path, preset)
|
|
copy_files(config_path, preset)
|
|
install_desktop(config_path, install_method)
|
|
|
|
print()
|
|
print(f"✓ Illuscape installed (preset: {preset})")
|
|
print()
|
|
print(" Open Inkscape to start using your Illustrator-style workspace.")
|
|
print(" To uninstall: python3 uninstall.py (or ./uninstall.sh on Linux/macOS)")
|
|
print()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|