diff --git a/.github/workflows/installerv2-ci.yml b/.github/workflows/installerv2-ci.yml new file mode 100644 index 00000000..7658b58f --- /dev/null +++ b/.github/workflows/installerv2-ci.yml @@ -0,0 +1,61 @@ +name: InstallerV2 CI + +on: + pull_request_target: + branches: [ main ] + workflow_dispatch: + +jobs: + build-and-smoke: + runs-on: macos-latest + steps: + - name: Checkout base (for context) + uses: actions/checkout@v4 + + - name: Checkout PR head + if: github.event_name == 'pull_request_target' + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + path: pr_head + + - name: Show workspace files (debug) + run: | + echo "pwd=$(pwd)" + ls -la + echo "PR head tree:" && ls -la pr_head || true + + - name: Build universal installer + shell: bash + run: | + if [[ -d pr_head ]]; then cd pr_head; fi + test -f InstallerV2/build/create-universal-installer.sh || { echo "InstallerV2 build script missing"; ls -la InstallerV2 || true; exit 1; } + bash InstallerV2/build/create-universal-installer.sh + ls -la dist + + - name: Smoke test (CLI, dry-run) + shell: bash + env: + CI: true + DTU_ANALYTICS_ENABLED: "0" + run: | + if [[ -d pr_head ]]; then cd pr_head; fi + ./dist/dtu-installer.sh --cli --dry-run + + - name: Smoke test (GUI, dry-run) + shell: bash + env: + CI: true + DTU_ANALYTICS_ENABLED: "0" + run: | + if [[ -d pr_head ]]; then cd pr_head; fi + ./dist/dtu-installer.sh --gui --dry-run + + - name: Direct run via repo entry (CLI, dry-run) + shell: bash + env: + CI: true + DTU_ANALYTICS_ENABLED: "0" + run: | + if [[ -d pr_head ]]; then cd pr_head; fi + ./InstallerV2/install.sh --cli --dry-run diff --git a/.github/workflows/installerv2-pr.yml b/.github/workflows/installerv2-pr.yml new file mode 100644 index 00000000..838dfca3 --- /dev/null +++ b/.github/workflows/installerv2-pr.yml @@ -0,0 +1,52 @@ +name: InstallerV2 PR + +on: + pull_request: + branches: [ main ] + paths: + - 'InstallerV2/**' + - '.github/workflows/installerv2-pr.yml' + workflow_dispatch: + +jobs: + build-and-smoke: + runs-on: macos-latest + steps: + - name: Checkout PR head + uses: actions/checkout@v4 + + - name: Show workspace files (debug) + run: | + echo "pwd=$(pwd)" + ls -la + + - name: Build universal installer + shell: bash + run: | + bash InstallerV2/build/create-universal-installer.sh + ls -la dist + + - name: Smoke test (CLI, dry-run) + shell: bash + env: + CI: true + DTU_ANALYTICS_ENABLED: "0" + run: | + ./dist/dtu-installer.sh --cli --dry-run + + - name: Smoke test (GUI, dry-run) + shell: bash + env: + CI: true + DTU_ANALYTICS_ENABLED: "0" + run: | + ./dist/dtu-installer.sh --gui --dry-run + + - name: Direct run via repo entry (CLI, dry-run) + shell: bash + env: + CI: true + DTU_ANALYTICS_ENABLED: "0" + run: | + ./InstallerV2/install.sh --cli --dry-run + diff --git a/.gitignore b/.gitignore index c30ef55f..72ac34f9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,12 +25,23 @@ __pycache__/ *.so .Python build/ +!InstallerV2/ +!InstallerV2/build/ +!InstallerV2/build/create-universal-installer.sh develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ +!InstallerV2/lib/ +!InstallerV2/lib/logging.sh +!InstallerV2/lib/config.sh +!InstallerV2/lib/platform.sh +!InstallerV2/lib/telemetry.sh +!InstallerV2/lib/ui-cli.sh +!InstallerV2/lib/ui-gui.sh +!InstallerV2/lib/core.sh lib64/ parts/ sdist/ @@ -106,4 +117,4 @@ docs/_build/ # Application specific *.dmg -*.app \ No newline at end of file +*.app diff --git a/InstallerV2/build/create-universal-installer.sh b/InstallerV2/build/create-universal-installer.sh new file mode 100644 index 00000000..918ad568 --- /dev/null +++ b/InstallerV2/build/create-universal-installer.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +DIST_DIR="$ROOT_DIR/../dist" +OUT="$DIST_DIR/dtu-installer.sh" + +mkdir -p "$DIST_DIR" + +# Simple concatenation build: create a single script that sources embedded parts. +# For now we just stitch files in a deterministic order; network downloads remain external. + +{ + echo "#!/usr/bin/env bash" + echo "set -euo pipefail" + echo "# Generated universal installer" + echo "EMBEDDED=1" + echo "ROOT_DIR=\"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)\"" + + echo "# --- logging ---" + cat "$ROOT_DIR/lib/logging.sh" + echo "# --- config ---" + cat "$ROOT_DIR/lib/config.sh" + echo "# --- platform ---" + cat "$ROOT_DIR/lib/platform.sh" + echo "# --- telemetry ---" + cat "$ROOT_DIR/lib/telemetry.sh" + echo "# --- ui cli ---" + cat "$ROOT_DIR/lib/ui-cli.sh" + echo "# --- ui gui ---" + cat "$ROOT_DIR/lib/ui-gui.sh" + + echo "# --- phases ---" + cat "$ROOT_DIR/phases/pre_install.sh" + cat "$ROOT_DIR/phases/post_install.sh" + + echo "# --- components ---" + cat "$ROOT_DIR/components/python.sh" + cat "$ROOT_DIR/components/vscode.sh" + cat "$ROOT_DIR/components/packages.sh" + + echo "# --- core ---" + cat "$ROOT_DIR/lib/core.sh" + echo 'main_core "$@"' +} > "$OUT" + +chmod +x "$OUT" +echo "Built: $OUT" + diff --git a/InstallerV2/components/packages.sh b/InstallerV2/components/packages.sh new file mode 100644 index 00000000..27c77686 --- /dev/null +++ b/InstallerV2/components/packages.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +packages_precheck() { + local pybin="$HOME/miniforge3/bin/python3" + if [[ ! -x "$pybin" ]]; then + ui_info "Packages precheck: Miniforge Python not found; packages will be installed when Python is ready." + return 0 + fi + + local tmpf + tmpf=$(mktemp) + cat > "$tmpf" <<'PY' +import importlib, sys +missing = [] +for p in sys.argv[1:]: + try: + importlib.import_module(p) + except Exception: + missing.append(p) +if missing: + print("MISSING:"+",".join(missing)) + sys.exit(1) +print("OK") +PY + if "$pybin" "$tmpf" "${DTU_PACKAGES[@]}" >/dev/null 2>&1; then + ui_info "Required packages already installed. Skipping." + rm -f "$tmpf" + return 1 + fi + rm -f "$tmpf" + ui_info "Some required packages missing. Will install." + return 0 +} + +packages_install() { + local conda_bin="$HOME/miniforge3/bin/conda" + [[ -x "$conda_bin" ]] || die "Conda not found; cannot install packages" + announce "Installing required packages via conda-forge" + "$conda_bin" install -y -c conda-forge "${DTU_PACKAGES[@]}" +} + diff --git a/InstallerV2/components/python.sh b/InstallerV2/components/python.sh new file mode 100644 index 00000000..e7bba3af --- /dev/null +++ b/InstallerV2/components/python.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +python_is_installed() { + [[ -x "$HOME/miniforge3/bin/python3" ]] +} + +python_precheck() { + if python_is_installed; then + ui_info "Python (Miniforge) already installed. Skipping." + return 1 + fi + ui_info "Python (Miniforge) not found. Will install." + return 0 +} + +python_install() { + local url installer prefix + url="$(miniforge_url_for_arch)" + installer="/tmp/miniforge-installer.sh" + prefix="$HOME/miniforge3" + + announce "Downloading Miniforge: $url" + curl -fsSL "$url" -o "$installer" + + announce "Installing Miniforge to $prefix" + bash "$installer" -b -p "$prefix" + + local conda_bin + conda_bin="$prefix/bin/conda" + [[ -x "$conda_bin" ]] || die "Conda not found after Miniforge install" + + announce "Configuring conda channels" + "$conda_bin" config --set channel_priority strict || true + "$conda_bin" config --remove channels defaults || true + "$conda_bin" config --add channels conda-forge || true + + announce "Ensuring Python ${DTU_PYTHON_VERSION}" + "$conda_bin" install -y "python=${DTU_PYTHON_VERSION}" + + ui_info "Miniforge installed at $prefix" +} + diff --git a/InstallerV2/components/vscode.sh b/InstallerV2/components/vscode.sh new file mode 100644 index 00000000..d254588c --- /dev/null +++ b/InstallerV2/components/vscode.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +vscode_is_installed() { + if command -v code >/dev/null 2>&1; then return 0; fi + if [[ -d "/Applications/Visual Studio Code.app" ]] || [[ -d "$HOME/Applications/Visual Studio Code.app" ]]; then return 0; fi + return 1 +} + +_resolve_code_bin() { + if command -v code >/dev/null 2>&1; then + command -v code + return 0 + fi + if [[ -x "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" ]]; then + echo "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" + return 0 + fi + if [[ -x "$HOME/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" ]]; then + echo "$HOME/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" + return 0 + fi + return 1 +} + +vscode_precheck() { + if vscode_is_installed; then + ui_info "VS Code already installed. Skipping." + return 1 + fi + ui_info "VS Code not found. Will install." + return 0 +} + +vscode_install() { + announce "Installing Visual Studio Code" + local target_app="/Applications" + if [[ ! -w "$target_app" ]]; then + target_app="$HOME/Applications" + mkdir -p "$target_app" + fi + + if command -v brew >/dev/null 2>&1; then + announce "Using Homebrew to install VS Code" + brew install --cask visual-studio-code || true + else + announce "Downloading VS Code (no Homebrew detected)" + local url zip tmp + url="https://update.code.visualstudio.com/latest/darwin/universal/stable" + tmp=$(mktemp -d) + zip="$tmp/vscode.zip" + curl -fsSL "$url" -o "$zip" + (cd "$tmp" && unzip -q "$zip") + # Expect "Visual Studio Code.app" folder + if [[ -d "$tmp/Visual Studio Code.app" ]]; then + rm -rf "$target_app/Visual Studio Code.app" + mv "$tmp/Visual Studio Code.app" "$target_app/" + else + warn "VS Code archive did not contain expected app bundle" + fi + rm -rf "$tmp" + fi + + # Try to install extensions + local codebin + if codebin=$(_resolve_code_bin); then + for ext in "${VSCODE_EXTENSIONS[@]}"; do + "$codebin" --install-extension "$ext" || true + done + ui_info "Installed VS Code extensions." + else + warn "VS Code CLI not found; could not install extensions automatically." + fi +} + diff --git a/InstallerV2/install.sh b/InstallerV2/install.sh new file mode 100644 index 00000000..94a88574 --- /dev/null +++ b/InstallerV2/install.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$SCRIPT_DIR" + +source "$ROOT_DIR/lib/logging.sh" +source "$ROOT_DIR/lib/config.sh" +source "$ROOT_DIR/lib/platform.sh" +source "$ROOT_DIR/lib/telemetry.sh" + +source "$ROOT_DIR/phases/pre_install.sh" +source "$ROOT_DIR/phases/post_install.sh" + +source "$ROOT_DIR/components/python.sh" +source "$ROOT_DIR/components/vscode.sh" +source "$ROOT_DIR/components/packages.sh" + +source "$ROOT_DIR/lib/core.sh" + +main_core "$@" + diff --git a/InstallerV2/lib/config.sh b/InstallerV2/lib/config.sh new file mode 100644 index 00000000..83777bf8 --- /dev/null +++ b/InstallerV2/lib/config.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# User-facing flags (parsed in core): +# --cli | --gui | --dry-run | --no-analytics | --prefix=PATH + +# Defaults align with existing MacOS/config.sh but isolated here for V2. +DTU_PYTHON_VERSION=${DTU_PYTHON_VERSION:-"3.12"} +DTU_PACKAGES=("dtumathtools" "pandas" "scipy" "statsmodels" "uncertainties") +VSCODE_EXTENSIONS=("ms-python.python" "ms-toolsai.jupyter" "tomoki1207.pdf") + +MINIFORGE_BASE_URL=${MINIFORGE_BASE_URL:-"https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-MacOSX"} +INSTALL_PREFIX=${INSTALL_PREFIX:-"$HOME"} + +# Analytics +DTU_ANALYTICS_ENABLED=${DTU_ANALYTICS_ENABLED:-"1"} + diff --git a/InstallerV2/lib/core.sh b/InstallerV2/lib/core.sh new file mode 100644 index 00000000..f16b20a5 --- /dev/null +++ b/InstallerV2/lib/core.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +if [[ -z "${EMBEDDED:-}" ]]; then + source "$ROOT_DIR/lib/logging.sh" + source "$ROOT_DIR/lib/config.sh" + source "$ROOT_DIR/lib/platform.sh" + source "$ROOT_DIR/lib/telemetry.sh" +fi + +# UI switching is controlled from bin/install.sh; here we just rely on funcs + +FINDINGS_FILE=${FINDINGS_FILE:-"/tmp/dtu_pre_install_findings.env"} + +if [[ -z "${EMBEDDED:-}" ]]; then + source "$ROOT_DIR/phases/pre_install.sh" + source "$ROOT_DIR/phases/post_install.sh" + + source "$ROOT_DIR/components/python.sh" + source "$ROOT_DIR/components/vscode.sh" + source "$ROOT_DIR/components/packages.sh" +fi + +parse_args() { + UI_MODE_AUTO=1 + DRY_RUN=0 + NO_ANALYTICS=0 + INSTALL_PREFIX_OVERRIDE="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --cli) UI_MODE="cli"; UI_MODE_AUTO=0; shift ;; + --gui) UI_MODE="gui"; UI_MODE_AUTO=0; shift ;; + --dry-run) DRY_RUN=1; shift ;; + --no-analytics) NO_ANALYTICS=1; shift ;; + --prefix=*) INSTALL_PREFIX_OVERRIDE="${1#*=}"; shift ;; + *) echo "Unknown flag: $1"; exit 2 ;; + esac + done + + if [[ -n "$INSTALL_PREFIX_OVERRIDE" ]]; then + INSTALL_PREFIX="$INSTALL_PREFIX_OVERRIDE" + fi + + if [[ "$NO_ANALYTICS" == "1" ]]; then + DTU_ANALYTICS_ENABLED=0 + fi +} + +auto_select_ui() { + if [[ -n "${UI_MODE:-}" ]]; then return; fi + if [[ -n "${DTU_UI:-}" ]]; then UI_MODE="$DTU_UI"; return; fi + if [[ -t 1 ]] && command -v osascript >/dev/null 2>&1; then + UI_MODE="gui" + else + UI_MODE="cli" + fi +} + +run_phase_pre() { + announce "Pre-checking system" + precheck_system "$FINDINGS_FILE" +} + +run_phase_install() { + announce "Installing components" + if python_precheck; then + [[ "$DRY_RUN" -eq 1 ]] || python_install + fi + if vscode_precheck; then + [[ "$DRY_RUN" -eq 1 ]] || vscode_install + fi + if packages_precheck; then + [[ "$DRY_RUN" -eq 1 ]] || packages_install + fi +} + +run_phase_post() { + announce "Verifying installation" + post_verify "$FINDINGS_FILE" +} + +main_core() { + require_macos || die "macOS required" + parse_args "$@" + auto_select_ui + + # shellcheck disable=SC1090 + if [[ -z "${EMBEDDED:-}" ]]; then + if [[ "$UI_MODE" == "gui" ]]; then + source "$ROOT_DIR/lib/ui-gui.sh" + else + source "$ROOT_DIR/lib/ui-cli.sh" + fi + fi + + log "Installer start: UI=$UI_MODE arch=$(detect_arch) macOS=$(detect_macos_version)" + + if [[ "$DTU_ANALYTICS_ENABLED" == "1" ]]; then + CONSENT=$(telemetry_consent) + [[ "$CONSENT" == "yes" ]] || DTU_ANALYTICS_ENABLED=0 + fi + + ui_info "DTU Installer starting (UI: $UI_MODE)" + run_phase_pre + run_phase_install + if run_phase_post; then + ui_info "Installation verified successfully." + log "Install OK" + else + ui_warn "Installation completed with issues. See log: $LOG_FILE" + log "Install issues detected" + fi +} diff --git a/InstallerV2/lib/logging.sh b/InstallerV2/lib/logging.sh new file mode 100644 index 00000000..b54f02e4 --- /dev/null +++ b/InstallerV2/lib/logging.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -o pipefail + +LOG_FILE=${LOG_FILE:-"/tmp/dtu_install_$(date +%Y%m%d_%H%M%S).log"} + +ts() { date '+%Y-%m-%d %H:%M:%S'; } + +log_raw() { + echo "[$(ts)] $*" | tee -a "$LOG_FILE" +} + +log() { log_raw "INFO: $*"; } +warn() { log_raw "WARN: $*" >&2; } +error() { log_raw "ERROR: $*" >&2; } + +die() { + error "$*" + exit 1 +} + +announce() { + echo "==> $*" | tee -a "$LOG_FILE" +} + diff --git a/InstallerV2/lib/platform.sh b/InstallerV2/lib/platform.sh new file mode 100644 index 00000000..aa477af0 --- /dev/null +++ b/InstallerV2/lib/platform.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +detect_arch() { + uname -m +} + +detect_macos_version() { + sw_vers -productVersion 2>/dev/null || echo "unknown" +} + +require_macos() { + if [[ "$(uname)" != "Darwin" ]]; then + echo "This installer supports macOS only." >&2 + return 1 + fi +} + +miniforge_url_for_arch() { + local arch + arch=$(detect_arch) + if [[ "$arch" == "arm64" ]]; then + echo "${MINIFORGE_BASE_URL}-arm64.sh" + else + echo "${MINIFORGE_BASE_URL}-x86_64.sh" + fi +} + diff --git a/InstallerV2/lib/telemetry.sh b/InstallerV2/lib/telemetry.sh new file mode 100644 index 00000000..5bb7c2ec --- /dev/null +++ b/InstallerV2/lib/telemetry.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +telemetry_consent() { + # Respect env/flags and CI contexts + if [[ "${DTU_ANALYTICS_ENABLED}" != "1" ]] || [[ -n "${CI:-}" ]]; then + echo "no" + return 0 + fi + + # If GUI chosen and osascript available, ask via dialog + if [[ "$UI_MODE" == "gui" ]] && command -v osascript >/dev/null 2>&1; then + local choice + choice=$(osascript -e 'display dialog "Allow anonymous usage analytics to improve the installer?" buttons {"No","Yes"} default button 2 with icon note' 2>/dev/null) + if echo "$choice" | grep -q "button returned:Yes"; then echo "yes"; else echo "no"; fi + return 0 + fi + + # CLI default to opt-in with a prompt + read -r -p "Allow anonymous analytics to improve the installer? [Y/n] " ans + ans=${ans:-Y} + if [[ "$ans" =~ ^[Yy]$ ]]; then echo "yes"; else echo "no"; fi +} + +telemetry_event() { + local event="$1"; shift + # Placeholder: integrate with existing `piwik_utility.sh` later + : +} diff --git a/InstallerV2/lib/ui-cli.sh b/InstallerV2/lib/ui-cli.sh new file mode 100644 index 00000000..579300bd --- /dev/null +++ b/InstallerV2/lib/ui-cli.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +ui_info() { echo "$*"; } +ui_warn() { echo "[WARN] $*" >&2; } +ui_error() { echo "[ERROR] $*" >&2; } + +ui_confirm() { + local prompt="$1" + read -r -p "$prompt [Y/n] " ans + ans=${ans:-Y} + [[ "$ans" =~ ^[Yy]$ ]] +} + diff --git a/InstallerV2/lib/ui-gui.sh b/InstallerV2/lib/ui-gui.sh new file mode 100644 index 00000000..37d06fc7 --- /dev/null +++ b/InstallerV2/lib/ui-gui.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +ui_info() { + if command -v osascript >/dev/null 2>&1; then + osascript -e "display notification \"$*\" with title \"DTU Installer\"" >/dev/null 2>&1 || true + else + echo "$*" + fi +} + +ui_warn() { + if command -v osascript >/dev/null 2>&1; then + osascript -e "display alert \"Warning\" message \"$*\"" >/dev/null 2>&1 || true + else + echo "[WARN] $*" >&2 + fi +} + +ui_error() { + if command -v osascript >/dev/null 2>&1; then + osascript -e "display alert \"Error\" message \"$*\" as critical" >/dev/null 2>&1 || true + else + echo "[ERROR] $*" >&2 + fi +} + +ui_confirm() { + local prompt="$1" + if command -v osascript >/dev/null 2>&1; then + local result + result=$(osascript -e "display dialog \"$prompt\" buttons {\"No\",\"Yes\"} default button 2" 2>/dev/null || true) + echo "$result" | grep -q "button returned:Yes" + else + read -r -p "$prompt [Y/n] " ans + ans=${ans:-Y} + [[ "$ans" =~ ^[Yy]$ ]] + fi +} + diff --git a/InstallerV2/phases/post_install.sh b/InstallerV2/phases/post_install.sh new file mode 100644 index 00000000..a3046df1 --- /dev/null +++ b/InstallerV2/phases/post_install.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +# Post-install verification. Returns 0 on full success, 1 if issues. + +post_verify() { + local findings_file="$1" + local ok=1 + + # Verify Python (Miniforge) + local pybin + pybin="$HOME/miniforge3/bin/python3" + if [[ -x "$pybin" ]]; then + log "Python found at $pybin: $($pybin --version 2>/dev/null | tr -d '\n')" + else + warn "Miniforge Python not found at $pybin" + ok=0 + fi + + # Verify packages + if [[ -x "$pybin" ]]; then + local test_script + test_script=$(cat <<'PY' +import importlib, sys +pkgs = " ,".join(sys.argv[1:]) +missing = [] +for p in sys.argv[1:]: + try: + importlib.import_module(p) + except Exception: + missing.append(p) +if missing: + print("MISSING:"+",".join(missing)) + sys.exit(1) +print("OK") +PY +) + local tmpf + tmpf=$(mktemp) + echo "$test_script" > "$tmpf" + if "$pybin" "$tmpf" "${DTU_PACKAGES[@]}" >/dev/null 2>&1; then + log "Required Python packages import successfully" + else + warn "Some required Python packages failed to import" + ok=0 + fi + rm -f "$tmpf" + fi + + # Verify VS Code and extensions + local codebin + if command -v code >/dev/null 2>&1; then + codebin="$(command -v code)" + elif [[ -x "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" ]]; then + codebin="/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" + elif [[ -x "$HOME/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" ]]; then + codebin="$HOME/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" + else + codebin="" + fi + + if [[ -n "$codebin" ]]; then + if "$codebin" --list-extensions 2>/dev/null | grep -q "ms-python.python"; then + log "VS Code present and Python extension installed" + else + warn "VS Code present but Python extension missing" + ok=0 + fi + else + warn "VS Code not found" + ok=0 + fi + + if [[ $ok -eq 1 ]]; then return 0; else return 1; fi +} + diff --git a/InstallerV2/phases/pre_install.sh b/InstallerV2/phases/pre_install.sh new file mode 100644 index 00000000..b2591db2 --- /dev/null +++ b/InstallerV2/phases/pre_install.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Pre-install system checks. Writes summary key=val pairs to a findings file. + +precheck_system() { + local out_file="$1" + : > "$out_file" + + local arch macos ver_ok + arch=$(uname -m) + macos=$(sw_vers -productVersion 2>/dev/null || echo "unknown") + + echo "ARCH=$arch" >> "$out_file" + echo "MACOS_VERSION=$macos" >> "$out_file" + + # Minimal supported macOS check (10.15+). Keep simple for now. + if command -v sw_vers >/dev/null 2>&1; then + IFS='.' read -r major minor patch <<< "${macos}.0" + if [[ ${major:-0} -gt 10 ]] || [[ ${major:-0} -eq 10 && ${minor:-0} -ge 15 ]]; then + ver_ok=1 + else + ver_ok=0 + fi + else + ver_ok=1 + fi + + echo "MACOS_SUPPORTED=$ver_ok" >> "$out_file" + + # Free space check (best-effort) + local freespace + freespace=$(df -Pk "$HOME" 2>/dev/null | awk 'NR==2 {print $4}') + echo "FREE_KB_HOME=${freespace:-unknown}" >> "$out_file" + + if [[ "${ver_ok}" != "1" ]]; then + warn "Detected macOS $macos; versions before 10.15 may not be supported." + fi + + return 0 +} +