- Add comprehensive version information system with git integration - Add `markitect version` and `markitect release` commands with multiple output formats - Add global `--version` flag for quick version checking - Create Python installer script with advanced options (install.py) - Create shell installer wrapper for easy installation (install.sh) - Add comprehensive installation documentation (INSTALL.md) - Support user and system-wide installations with virtual environments - Include development mode installation with test dependencies - Add installation status checking and uninstall functionality Commands added: - `markitect --version` - Quick version display - `markitect version [--short]` - Detailed version information - `markitect release [--format text|json|yaml]` - Release information Installer features: - Automatic virtual environment creation - Symbolic link management for global access - Custom installation paths and prefixes - Development mode with test dependencies - Installation validation and troubleshooting Resolves #80 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
388 lines
14 KiB
Python
388 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
MarkiTect Installer
|
|
|
|
This script provides an easy way to install MarkiTect and make it available
|
|
system-wide. It handles virtual environment creation, dependency installation,
|
|
and creates symbolic links to make the commands available from anywhere.
|
|
|
|
Usage:
|
|
python install.py [options]
|
|
|
|
Options:
|
|
--prefix PATH Installation prefix (default: ~/.local)
|
|
--system Install system-wide (requires sudo, uses /usr/local)
|
|
--venv-dir PATH Custom virtual environment directory
|
|
--no-symlinks Don't create symbolic links (manual PATH setup required)
|
|
--force Force reinstallation over existing installation
|
|
--dev Install in development mode with test dependencies
|
|
--check Check if MarkiTect is already installed
|
|
--uninstall Uninstall MarkiTect
|
|
--help Show this help message
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import shutil
|
|
import argparse
|
|
from pathlib import Path
|
|
import tempfile
|
|
|
|
|
|
class MarkiTectInstaller:
|
|
"""MarkiTect installation manager."""
|
|
|
|
def __init__(self, prefix=None, system=False, venv_dir=None, force=False, dev=False):
|
|
self.system = system
|
|
self.force = force
|
|
self.dev = dev
|
|
|
|
# Determine installation paths
|
|
if system:
|
|
self.prefix = Path("/usr/local")
|
|
self.bin_dir = self.prefix / "bin"
|
|
self.venv_dir = Path(venv_dir) if venv_dir else self.prefix / "lib" / "markitect"
|
|
else:
|
|
self.prefix = Path(prefix) if prefix else Path.home() / ".local"
|
|
self.bin_dir = self.prefix / "bin"
|
|
self.venv_dir = Path(venv_dir) if venv_dir else self.prefix / "lib" / "markitect"
|
|
|
|
self.project_dir = Path(__file__).parent.absolute()
|
|
|
|
def check_requirements(self):
|
|
"""Check system requirements."""
|
|
print("🔍 Checking system requirements...")
|
|
|
|
# Check Python version
|
|
if sys.version_info < (3, 8):
|
|
print("❌ Python 3.8 or higher is required")
|
|
sys.exit(1)
|
|
print(f"✅ Python {sys.version.split()[0]} found")
|
|
|
|
# Check if pip is available
|
|
try:
|
|
subprocess.run([sys.executable, "-m", "pip", "--version"],
|
|
check=True, capture_output=True)
|
|
print("✅ pip is available")
|
|
except subprocess.CalledProcessError:
|
|
print("❌ pip is not available. Please install pip first.")
|
|
sys.exit(1)
|
|
|
|
# Check if git is available (optional)
|
|
try:
|
|
subprocess.run(["git", "--version"], check=True, capture_output=True)
|
|
print("✅ git is available")
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
print("⚠️ git is not available (optional for version info)")
|
|
|
|
def check_existing_installation(self):
|
|
"""Check if MarkiTect is already installed."""
|
|
# Check for existing venv
|
|
if self.venv_dir.exists():
|
|
print(f"📁 Existing installation found at {self.venv_dir}")
|
|
return True
|
|
|
|
# Check for existing binaries
|
|
markitect_bin = self.bin_dir / "markitect"
|
|
if markitect_bin.exists():
|
|
print(f"📁 Existing binary found at {markitect_bin}")
|
|
return True
|
|
|
|
return False
|
|
|
|
def create_directories(self):
|
|
"""Create necessary directories."""
|
|
print(f"📁 Creating directories...")
|
|
|
|
if self.system and not os.access(self.prefix, os.W_OK):
|
|
print("❌ System installation requires sudo privileges")
|
|
print(" Please run with sudo or choose a different installation prefix")
|
|
sys.exit(1)
|
|
|
|
self.prefix.mkdir(parents=True, exist_ok=True)
|
|
self.bin_dir.mkdir(parents=True, exist_ok=True)
|
|
print(f"✅ Created directories in {self.prefix}")
|
|
|
|
def create_virtual_environment(self):
|
|
"""Create and set up virtual environment."""
|
|
print(f"🐍 Creating virtual environment at {self.venv_dir}")
|
|
|
|
if self.venv_dir.exists():
|
|
if self.force:
|
|
print(f"🗑️ Removing existing installation...")
|
|
shutil.rmtree(self.venv_dir)
|
|
else:
|
|
print("❌ Virtual environment already exists. Use --force to overwrite.")
|
|
sys.exit(1)
|
|
|
|
# Create virtual environment
|
|
subprocess.run([
|
|
sys.executable, "-m", "venv", str(self.venv_dir)
|
|
], check=True)
|
|
|
|
# Get paths to venv executables
|
|
if sys.platform == "win32":
|
|
venv_python = self.venv_dir / "Scripts" / "python.exe"
|
|
venv_pip = self.venv_dir / "Scripts" / "pip.exe"
|
|
else:
|
|
venv_python = self.venv_dir / "bin" / "python"
|
|
venv_pip = self.venv_dir / "bin" / "pip"
|
|
|
|
# Upgrade pip
|
|
print("📦 Upgrading pip...")
|
|
subprocess.run([
|
|
str(venv_pip), "install", "--upgrade", "pip", "setuptools", "wheel"
|
|
], check=True)
|
|
|
|
return venv_python, venv_pip
|
|
|
|
def install_markitect(self, venv_python, venv_pip):
|
|
"""Install MarkiTect in the virtual environment."""
|
|
print("📦 Installing MarkiTect...")
|
|
|
|
install_cmd = [str(venv_pip), "install"]
|
|
|
|
if self.dev:
|
|
print("🛠️ Installing in development mode with test dependencies...")
|
|
# Install in editable mode from current directory
|
|
install_cmd.extend(["-e", str(self.project_dir)])
|
|
|
|
# Install test dependencies
|
|
subprocess.run(install_cmd, check=True)
|
|
subprocess.run([
|
|
str(venv_pip), "install", "pytest", "pytest-cov", "black", "flake8", "mypy"
|
|
], check=True)
|
|
else:
|
|
# Install from current directory
|
|
install_cmd.append(str(self.project_dir))
|
|
subprocess.run(install_cmd, check=True)
|
|
|
|
print("✅ MarkiTect installed successfully")
|
|
|
|
def create_symlinks(self, no_symlinks=False):
|
|
"""Create symbolic links for global access."""
|
|
if no_symlinks:
|
|
print("⚠️ Skipping symbolic link creation")
|
|
self.show_manual_setup()
|
|
return
|
|
|
|
print("🔗 Creating symbolic links...")
|
|
|
|
# Get venv bin directory
|
|
if sys.platform == "win32":
|
|
venv_bin = self.venv_dir / "Scripts"
|
|
exe_suffix = ".exe"
|
|
else:
|
|
venv_bin = self.venv_dir / "bin"
|
|
exe_suffix = ""
|
|
|
|
# Commands to link
|
|
commands = ["markitect", "tddai", "issue"]
|
|
|
|
for cmd in commands:
|
|
src = venv_bin / f"{cmd}{exe_suffix}"
|
|
dst = self.bin_dir / cmd
|
|
|
|
if src.exists():
|
|
# Remove existing symlink/file
|
|
if dst.exists() or dst.is_symlink():
|
|
dst.unlink()
|
|
|
|
# Create symlink
|
|
try:
|
|
dst.symlink_to(src)
|
|
print(f"✅ Created symlink: {dst} -> {src}")
|
|
except OSError:
|
|
# Fallback: create wrapper script
|
|
self.create_wrapper_script(dst, src)
|
|
else:
|
|
print(f"⚠️ Command {cmd} not found in virtual environment")
|
|
|
|
def create_wrapper_script(self, dst, src):
|
|
"""Create a wrapper script when symlinks aren't available."""
|
|
print(f"🔧 Creating wrapper script: {dst}")
|
|
|
|
if sys.platform == "win32":
|
|
# Windows batch file
|
|
dst = dst.with_suffix(".bat")
|
|
content = f'@echo off\n"{src}" %*\n'
|
|
else:
|
|
# Unix shell script
|
|
content = f'#!/bin/bash\nexec "{src}" "$@"\n'
|
|
|
|
dst.write_text(content)
|
|
if sys.platform != "win32":
|
|
os.chmod(dst, 0o755)
|
|
|
|
def show_manual_setup(self):
|
|
"""Show manual PATH setup instructions."""
|
|
print("\n📋 Manual Setup Instructions:")
|
|
print("=" * 50)
|
|
print(f"Add the following to your PATH environment variable:")
|
|
print(f" {self.venv_dir / 'bin'}")
|
|
print()
|
|
print("For bash/zsh, add this line to ~/.bashrc or ~/.zshrc:")
|
|
print(f' export PATH="{self.venv_dir / "bin"}:$PATH"')
|
|
print()
|
|
|
|
def test_installation(self):
|
|
"""Test the installation."""
|
|
print("🧪 Testing installation...")
|
|
|
|
# Test markitect command
|
|
try:
|
|
markitect_bin = self.bin_dir / "markitect"
|
|
if not markitect_bin.exists():
|
|
# Try direct venv path
|
|
if sys.platform == "win32":
|
|
markitect_bin = self.venv_dir / "Scripts" / "markitect.exe"
|
|
else:
|
|
markitect_bin = self.venv_dir / "bin" / "markitect"
|
|
|
|
result = subprocess.run([
|
|
str(markitect_bin), "version", "--short"
|
|
], capture_output=True, text=True, check=True)
|
|
|
|
version = result.stdout.strip()
|
|
print(f"✅ MarkiTect installed successfully - version {version}")
|
|
return True
|
|
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
print(f"❌ Installation test failed: {e}")
|
|
return False
|
|
|
|
def uninstall(self):
|
|
"""Uninstall MarkiTect."""
|
|
print("🗑️ Uninstalling MarkiTect...")
|
|
|
|
removed_something = False
|
|
|
|
# Remove virtual environment
|
|
if self.venv_dir.exists():
|
|
print(f"🗑️ Removing virtual environment: {self.venv_dir}")
|
|
shutil.rmtree(self.venv_dir)
|
|
removed_something = True
|
|
|
|
# Remove symlinks
|
|
commands = ["markitect", "tddai", "issue"]
|
|
for cmd in commands:
|
|
for bin_path in [self.bin_dir / cmd, self.bin_dir / f"{cmd}.bat"]:
|
|
if bin_path.exists() or bin_path.is_symlink():
|
|
print(f"🗑️ Removing: {bin_path}")
|
|
bin_path.unlink()
|
|
removed_something = True
|
|
|
|
if removed_something:
|
|
print("✅ MarkiTect uninstalled successfully")
|
|
else:
|
|
print("⚠️ No MarkiTect installation found")
|
|
|
|
def install(self, no_symlinks=False):
|
|
"""Perform the complete installation."""
|
|
print("🚀 Installing MarkiTect")
|
|
print("=" * 50)
|
|
|
|
self.check_requirements()
|
|
|
|
if not self.force and self.check_existing_installation():
|
|
print("❌ MarkiTect is already installed. Use --force to reinstall.")
|
|
sys.exit(1)
|
|
|
|
self.create_directories()
|
|
venv_python, venv_pip = self.create_virtual_environment()
|
|
self.install_markitect(venv_python, venv_pip)
|
|
self.create_symlinks(no_symlinks)
|
|
|
|
print()
|
|
if self.test_installation():
|
|
print("🎉 Installation completed successfully!")
|
|
print()
|
|
print("You can now use MarkiTect from anywhere:")
|
|
print(" markitect --help")
|
|
print(" markitect version")
|
|
print(" tddai --help")
|
|
print(" issue --help")
|
|
else:
|
|
print("⚠️ Installation completed but tests failed")
|
|
self.show_manual_setup()
|
|
|
|
def check_installation_status(self):
|
|
"""Check current installation status."""
|
|
print("🔍 MarkiTect Installation Status")
|
|
print("=" * 50)
|
|
|
|
# Check virtual environment
|
|
if self.venv_dir.exists():
|
|
print(f"✅ Virtual environment: {self.venv_dir}")
|
|
else:
|
|
print(f"❌ Virtual environment: Not found at {self.venv_dir}")
|
|
|
|
# Check binaries
|
|
commands = ["markitect", "tddai", "issue"]
|
|
for cmd in commands:
|
|
bin_path = self.bin_dir / cmd
|
|
if bin_path.exists():
|
|
print(f"✅ {cmd}: {bin_path}")
|
|
else:
|
|
print(f"❌ {cmd}: Not found at {bin_path}")
|
|
|
|
# Try to get version
|
|
try:
|
|
result = subprocess.run([
|
|
"markitect", "version", "--short"
|
|
], capture_output=True, text=True)
|
|
if result.returncode == 0:
|
|
version = result.stdout.strip()
|
|
print(f"✅ Working installation: version {version}")
|
|
else:
|
|
print("❌ Installation found but not working")
|
|
except FileNotFoundError:
|
|
print("❌ markitect command not available in PATH")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="MarkiTect Installer",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=__doc__.split('\n\n')[1] # Show usage from docstring
|
|
)
|
|
|
|
parser.add_argument("--prefix", type=Path,
|
|
help="Installation prefix (default: ~/.local)")
|
|
parser.add_argument("--system", action="store_true",
|
|
help="Install system-wide (requires sudo)")
|
|
parser.add_argument("--venv-dir", type=Path,
|
|
help="Custom virtual environment directory")
|
|
parser.add_argument("--no-symlinks", action="store_true",
|
|
help="Don't create symbolic links")
|
|
parser.add_argument("--force", action="store_true",
|
|
help="Force reinstallation")
|
|
parser.add_argument("--dev", action="store_true",
|
|
help="Install in development mode")
|
|
parser.add_argument("--check", action="store_true",
|
|
help="Check installation status")
|
|
parser.add_argument("--uninstall", action="store_true",
|
|
help="Uninstall MarkiTect")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Create installer instance
|
|
installer = MarkiTectInstaller(
|
|
prefix=args.prefix,
|
|
system=args.system,
|
|
venv_dir=args.venv_dir,
|
|
force=args.force,
|
|
dev=args.dev
|
|
)
|
|
|
|
# Handle different actions
|
|
if args.check:
|
|
installer.check_installation_status()
|
|
elif args.uninstall:
|
|
installer.uninstall()
|
|
else:
|
|
installer.install(no_symlinks=args.no_symlinks)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |