From 922ab997e2143c4bc1c1592972769a5fca4a0ccb Mon Sep 17 00:00:00 2001 From: Thibault Falque Date: Tue, 26 Aug 2025 16:03:24 +0200 Subject: [PATCH 1/3] feat(install.sh): Allow to install xcsp-launcher on Home Directory --- .github/workflows/release.yml | 20 ++ install.sh | 387 ++++++++++++++++++++++++++++++++++ xcsp/solver/solver.py | 9 +- xcsp/utils/paths.py | 10 +- 4 files changed, 421 insertions(+), 5 deletions(-) create mode 100755 install.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f72651b..7ff235b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,7 +76,27 @@ jobs: # choco pack xcsp-launcher.nuspec # Pop-Location # choco push chocolatey/xcsp-launcher.*.nupkg --api-key ${{ secrets.CHOCOLATEY_API_KEY }} --source="https://push.chocolatey.org/" + - name: Rename Linux binary + if: matrix.os == 'ubuntu-latest' + run: | + if [ -f dist/xcsp ]; then + cp dist/xcsp dist/xcsp-linux-x86_64 + fi + + - name: Expose macOS universal binary under dist + if: matrix.os == 'macos-latest' + run: | + mkdir -p dist + # dist-universal/xcsp est déjà l’universal (arm64+x86_64) signé+notarisé + cp dist-universal/xcsp dist/xcsp-macos-universal + - name: Rename Windows binary + if: matrix.os == 'windows-latest' + shell: bash + run: | + if [ -f dist/xcsp.exe ]; then + cp dist/xcsp.exe dist/xcsp-windows-x86_64.exe + fi - name: Upload built binaries and packages uses: actions/upload-artifact@v4 with: diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..98bbdbf --- /dev/null +++ b/install.sh @@ -0,0 +1,387 @@ +#!/usr/bin/env bash +# XCSP Launcher home-directory installer +# - Installs xcsp into ~/.local/opt/xcsp-launcher/ and symlinks ~/.local/bin/xcsp +# - Verifies checksums with cosign (if available) and sha256sum +# - Installs the XCSP3 solution checker jar under ~/.local/share/xcsp-launcher/tools/ +# - Fetches solver config files (*.solver.yaml) into ~/.config/xcsp-launcher/solvers +# - Offers interactive installation of selected solvers using `xcsp install -c ` +# +# Usage: +# ./install.sh [--version vX.Y.Z] [--prefix ~/.local] [--no-verify] +# [--cosign-identity ""] [--yes] +# +# Defaults: +# --prefix $HOME/.local +# --cosign-identity "https://github.com/CPToolset/xcsp-launcher/.github/workflows/release.yml@refs/heads/main" +# --version latest GitHub release +# +# Notes: +# * Requires: bash, curl, tar, sha256sum +# * Optional: cosign (for signature verification), git or tar/unzip (to fetch solver configs) +# * Supports Linux and macOS. Windows is not supported by this script. + +set -euo pipefail + +# ---------------------------- +# Configurable defaults +# ---------------------------- +REPO_SLUG="CPToolset/XCSP-Launcher" +IDENTITY_DEFAULT="https://github.com/CPToolset/xcsp-launcher/.github/workflows/release.yml@refs/heads/main" +PREFIX="${HOME}/.local" +RELEASE_TAG="" # empty -> resolve latest +COSIGN_IDENTITY="${IDENTITY_DEFAULT}" +NO_VERIFY="false" +ASSUME_YES="false" + +# ---------------------------- +# Basic colored output helpers +# ---------------------------- +bold() { printf "\033[1m%s\033[0m\n" "$*"; } +info() { printf "ℹ️ %s\n" "$*"; } +ok() { printf "✅ %s\n" "$*"; } +warn() { printf "⚠️ %s\n" "$*"; } +err() { printf "❌ %s\n" "$*" >&2; } +die() { err "$*"; exit 1; } + +# Simple spinner for long-running steps (use: long_cmd & spin $! "Message") +spin() { + local pid="$1"; shift + local msg="$*" + local frames='|/-\' + local i=0 + printf "%s " "$msg" + while kill -0 "$pid" 2>/dev/null; do + i=$(( (i+1) % 4 )) + printf "\r%s %s" "$msg" "${frames:$i:1}" + sleep 0.1 + done + printf "\r%s … done\n" "$msg" +} + +# ---------------------------- +# Parse CLI args +# ---------------------------- +while [[ $# -gt 0 ]]; do + case "$1" in + --version) RELEASE_TAG="$2"; shift 2 ;; + --prefix) PREFIX="$2"; shift 2 ;; + --no-verify) NO_VERIFY="true"; shift ;; + --cosign-identity) COSIGN_IDENTITY="$2"; shift 2 ;; + --yes|--assume-yes) ASSUME_YES="true"; shift ;; + -h|--help) + cat </dev/null 2>&1 || die "Missing command: $1"; } +need_cmd curl +need_cmd tar +need_cmd sha256sum + +HAS_COSIGN="false" +if command -v cosign >/dev/null 2>&1; then + HAS_COSIGN="true" +fi + +# ---------------------------- +# OS / ARCH detection +# ---------------------------- +OS="$(uname -s)" +ARCH="$(uname -m)" +case "$OS" in + Linux|Darwin) : ;; + *) die "Unsupported OS: $OS (only Linux/macOS supported)" ;; +esac + +# ---------------------------- +# Resolve release tag (latest if empty) +# ---------------------------- +if [[ -z "${RELEASE_TAG}" ]]; then + info "Resolving latest release tag…" + # Parse from GitHub API without jq + RELEASE_TAG="$(curl -fsSL "https://api.github.com/repos/${REPO_SLUG}/releases/latest" \ + | awk -F'"' '/"tag_name":/ {print $4}')" + [[ -n "${RELEASE_TAG}" ]] || die "Could not determine latest release tag" + ok "Latest release: ${RELEASE_TAG}" +else + info "Using requested release: ${RELEASE_TAG}" +fi + +# ---------------------------- +# Build asset URLs for this OS +# ---------------------------- +BASE_URL="https://github.com/${REPO_SLUG}/releases/download/${RELEASE_TAG}" +CHECKSUMS_TXT="${BASE_URL}/checksums.txt" +CHECKSUMS_SIG="${BASE_URL}/checksums.txt.sig" +CHECKSUMS_PEM="${BASE_URL}/checksums.txt.pem" + +ASSET_NAME="" +IS_TARBALL="false" + +if [[ "$OS" == "Linux" ]]; then + # Prefer the standalone linux binary named 'xcsp' + ASSET_NAME="xcsp" +elif [[ "$OS" == "Darwin" ]]; then + # Prefer 'xcsp-macos', fallback to 'xcsp--macos.tar.gz' + CANDIDATE1="xcsp-macos" + CANDIDATE2="xcsp-${RELEASE_TAG#v}-macos.tar.gz" + # HEAD to see which exists + if curl -fsI "${BASE_URL}/${CANDIDATE1}" >/dev/null 2>&1; then + ASSET_NAME="${CANDIDATE1}" + elif curl -fsI "${BASE_URL}/${CANDIDATE2}" >/dev/null 2>&1; then + ASSET_NAME="${CANDIDATE2}" + IS_TARBALL="true" + else + die "No macOS asset found for ${RELEASE_TAG}" + fi +fi + +ASSET_URL="${BASE_URL}/${ASSET_NAME}" + +# ---------------------------- +# Download dir +# ---------------------------- +WORK_DIR="$(mktemp -d)" +trap 'rm -rf "${WORK_DIR}"' EXIT + +# ---------------------------- +# Download files with progress bars +# ---------------------------- +download() { + local url="$1" out="$2" + curl -fL --progress-bar -o "$out" "$url" +} + +bold "1) Downloading release artifacts (${RELEASE_TAG})" +info "Release: ${BASE_URL}" +download "${ASSET_URL}" "${WORK_DIR}/${ASSET_NAME}" +download "${CHECKSUMS_TXT}" "${WORK_DIR}/checksums.txt" +download "${CHECKSUMS_SIG}" "${WORK_DIR}/checksums.txt.sig" +download "${CHECKSUMS_PEM}" "${WORK_DIR}/checksums.txt.pem" +ok "Artifacts downloaded to ${WORK_DIR}" + +# ---------------------------- +# Verify checksums with cosign + sha256 +# ---------------------------- +if [[ "${NO_VERIFY}" == "false" ]]; then + bold "2) Verifying checksums" + if [[ "${HAS_COSIGN}" == "true" ]]; then + info "cosign present: verifying the signed checksums.txt" + set +e + cosign verify-blob \ + --cert "${WORK_DIR}/checksums.txt.pem" \ + --signature "${WORK_DIR}/checksums.txt.sig" \ + --certificate-identity "${COSIGN_IDENTITY}" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ + "${WORK_DIR}/checksums.txt" >/dev/null 2>&1 + CS_RC=$? + set -e + if [[ $CS_RC -ne 0 ]]; then + die "cosign verification failed. You can rerun with --no-verify to bypass." + fi + ok "cosign: Verified OK" + else + warn "cosign not found; skipping signature verification. (Install cosign or rerun with --no-verify to silence this warning.)" + fi + + info "sha256sum: checking integrity of downloaded asset" + # Only check the specific file; ignore missing others + ( cd "${WORK_DIR}" && sha256sum --ignore-missing -c checksums.txt ) | grep -E "${ASSET_NAME}: OK" >/dev/null \ + || die "sha256 mismatch for ${ASSET_NAME}" + ok "sha256sum: ${ASSET_NAME} matches checksums.txt" +else + warn "Verification disabled by --no-verify" +fi + +# ---------------------------- +# Install under ~/.local/opt/xcsp-launcher/ +# ---------------------------- +bold "3) Installing XCSP Launcher into home directory" +DEST_DIR="${OPT_BASE}/${RELEASE_TAG#v}" +mkdir -p "${DEST_DIR}" + +if [[ "${IS_TARBALL}" == "true" ]]; then + info "Extracting tarball to ${DEST_DIR}" + tar -xzf "${WORK_DIR}/${ASSET_NAME}" -C "${DEST_DIR}" --strip-components=1 + # The tarball should contain a binary named 'xcsp' + [[ -x "${DEST_DIR}/xcsp" ]] || die "Expected 'xcsp' binary not found in tarball" +else + info "Placing binary in ${DEST_DIR}" + install -m 0755 "${WORK_DIR}/${ASSET_NAME}" "${DEST_DIR}/xcsp" +fi + +ln -sfn "${DEST_DIR}/xcsp" "${BIN_DIR}/xcsp" +ok "Installed: ${DEST_DIR}/xcsp" +ok "Symlinked: ${BIN_DIR}/xcsp -> ${DEST_DIR}/xcsp" + +# ---------------------------- +# Ensure PATH contains ~/.local/bin +# ---------------------------- +if ! command -v xcsp >/dev/null 2>&1; then + warn "xcsp not found in PATH." + echo + echo "Add this line to your shell profile (e.g., ~/.bashrc or ~/.zshrc):" + echo " export PATH=\"${BIN_DIR}:\$PATH\"" + echo +fi + +# ---------------------------- +# Install XCSP3 solution checker JAR +# ---------------------------- +bold "4) Installing XCSP3 solution checker" +CHECKER_URL="https://raw.githubusercontent.com/CPToolset/XCSP-Launcher/main/xcsp/tools/xcsp3-solutionChecker-2.5.jar" +CHECKER_OUT="${TOOLS_DIR}/xcsp3-solutionChecker-2.5.jar" +download "${CHECKER_URL}" "${CHECKER_OUT}" +chmod 0644 "${CHECKER_OUT}" +ok "Checker installed at ${CHECKER_OUT}" + +# ---------------------------- +# Fetch solver config files (*.solver.yaml) +# - Try git for speed; otherwise download tarball archive +# ---------------------------- +bold "5) Fetching solver configuration files into ${SOLVERS_DIR}" +TMP_SOLV="${WORK_DIR}/metrics-solvers" +mkdir -p "${TMP_SOLV}" + +fetch_solvers_via_git() { + command -v git >/dev/null 2>&1 || return 1 + git clone --depth 1 https://github.com/crillab/metrics-solvers.git "${TMP_SOLV}" >/dev/null 2>&1 || return 1 + return 0 +} +fetch_solvers_via_tar() { + local tar_url="https://github.com/crillab/metrics-solvers/archive/refs/heads/main.tar.gz" + download "${tar_url}" "${WORK_DIR}/metrics-solvers.tar.gz" + mkdir -p "${TMP_SOLV}" + tar -xzf "${WORK_DIR}/metrics-solvers.tar.gz" -C "${WORK_DIR}" + # Extracted folder name: + local root="${WORK_DIR}/metrics-solvers-main" + [[ -d "${root}" ]] || die "Unexpected archive layout for metrics-solvers" + mv "${root}" "${TMP_SOLV}" +} + +if fetch_solvers_via_git; then + ok "Cloned metrics-solvers via git" +else + warn "git not available or clone failed; falling back to tarball download" + fetch_solvers_via_tar + ok "Downloaded metrics-solvers archive" +fi + +# Copy any *.solver.yaml files found anywhere in the repo into SOLVERS_DIR +FOUND_COUNT=0 +while IFS= read -r -d '' f; do + cp -f "$f" "${SOLVERS_DIR}/" + FOUND_COUNT=$((FOUND_COUNT+1)) +done < <(find "${TMP_SOLV}" -type f -name "*.solver.yaml" -print0) + +if [[ "${FOUND_COUNT}" -gt 0 ]]; then + ok "Copied ${FOUND_COUNT} solver config(s) to ${SOLVERS_DIR}" +else + warn "No *.solver.yaml found in metrics-solvers repository." +fi + +# ---------------------------- +# Final checks & optional interactive solver install +# ---------------------------- +bold "6) Post-install checks" +if command -v xcsp >/dev/null 2>&1; then + ok "Command 'xcsp' is available: $(command -v xcsp)" + xcsp --version || true +else + warn "'xcsp' is not yet on PATH. You can run it via: ${DEST_DIR}/xcsp" +fi + +# Offer interactive solver installation if configs exist +install_solvers_menu() { + echo + bold "Optional: Install solvers from configs in ${SOLVERS_DIR}" + mapfile -t CFGS < <(find "${SOLVERS_DIR}" -maxdepth 1 -type f -name "*.solver.yaml" | sort) + if [[ ${#CFGS[@]} -eq 0 ]]; then + info "No solver config files (*.solver.yaml) found. Skipping." + return 0 + fi + + echo "Available solver config files:" + local i=1 + for f in "${CFGS[@]}"; do + echo " [$i] $(basename "$f")" + i=$((i+1)) + done + echo + if [[ "${ASSUME_YES}" == "true" ]]; then + info "--yes provided: skipping interactive selection." + return 0 + fi + read -rp "Enter numbers to install (space-separated, empty to skip): " -a picks + if [[ ${#picks[@]} -eq 0 ]]; then + info "No selection. Skipping solver installation." + return 0 + fi + for p in "${picks[@]}"; do + if [[ "$p" =~ ^[0-9]+$ ]] && (( p>=1 && p<=${#CFGS[@]} )); then + cfg="${CFGS[$((p-1))]}" + echo + info "Installing solver from $(basename "$cfg")" + # Use xcsp CLI to install + set +e + xcsp install -c "$cfg" + rc=$? + set -e + if [[ $rc -eq 0 ]]; then + ok "Installed from $(basename "$cfg")" + else + warn "Install failed for $(basename "$cfg") (exit $rc)" + fi + else + warn "Invalid selection: $p (ignored)" + fi + done +} + +install_solvers_menu + +echo +ok "XCSP Launcher ${RELEASE_TAG} installed successfully." +echo "Data dir: ${DATA_DIR}" +echo "Config dir: ${CONFIG_DIR}" +echo "Cache dir: ${XDG_CACHE_HOME}/xcsp-launcher" +echo "Binary: ${DEST_DIR}/xcsp (symlinked from ${BIN_DIR}/xcsp)" diff --git a/xcsp/solver/solver.py b/xcsp/solver/solver.py index d7a0d46..a73e894 100644 --- a/xcsp/solver/solver.py +++ b/xcsp/solver/solver.py @@ -24,7 +24,7 @@ from xcsp.solver.cache import CACHE from xcsp.utils.json import CustomEncoder -from xcsp.utils.paths import get_system_tools_dir +import xcsp.utils.paths as paths from xcsp.utils.system import kill_process, term_process ANSWER_PREFIX = "s" + chr(32) @@ -350,12 +350,17 @@ def solve(self, instance_path, keep_solver_output=False, check=False, delay=5): if check and self._solutions is not None and len(self._solutions["assignments"]) > 0: logger.info("Checking solution....") solution_checker_jar = None + all_paths = paths.get_system_tools_dir() + all_paths.extend([paths.get_user_tools_dir()]) + for st in all_paths: + logger.debug("Searching for solution checker in: " + str(st)) - for st in get_system_tools_dir(): if not st.exists(): + logger.debug("No solution checker found") continue p = st / "xcsp3-solutionChecker-2.5.jar" if not p.exists(): + logger.debug(f"Solution checker jar not found at {p}") continue solution_checker_jar=p diff --git a/xcsp/utils/paths.py b/xcsp/utils/paths.py index a1a41c0..6b5948b 100644 --- a/xcsp/utils/paths.py +++ b/xcsp/utils/paths.py @@ -7,7 +7,7 @@ import os import sys from pathlib import Path -from typing import Iterable +from typing import Iterable, List from platformdirs import user_cache_dir, user_config_dir, user_data_dir from rich.console import Console @@ -62,7 +62,7 @@ def get_system_config_dir() -> list[Path]: return [Path(f"/usr/share/{__title__}/configs")] -def get_system_tools_dir() -> Iterable[Path]: +def get_system_tools_dir() -> List[Path]: """Return the system-wide directory for external tools. This depends on the operating system: @@ -80,6 +80,10 @@ def get_system_tools_dir() -> Iterable[Path]: else: return [Path(f"/usr/share/{__title__}/tools")] +def get_user_tools_dir() -> Path: + """Return the user-specific directory for external tools.""" + return Path(user_data_dir(__title__, __title__)) / "tools" + def print_path_summary(): """Print a summary of important XCSP Launcher paths using Rich.""" console = Console(width=200) @@ -113,5 +117,5 @@ def __exit__(self, etype, value, traceback): os.chdir(self.saved_path) # Ensure important directories exist at startup -for path in [get_cache_dir(), get_solver_install_dir(), get_solver_config_dir(), get_user_preferences_dir()]: +for path in [get_cache_dir(), get_solver_install_dir(), get_solver_config_dir(), get_user_preferences_dir(), get_user_tools_dir()]: path.mkdir(parents=True, exist_ok=True) From 4ee7cdc356dd32094b8378bd4080f539041d4463 Mon Sep 17 00:00:00 2001 From: Thibault Falque Date: Tue, 26 Aug 2025 16:05:20 +0200 Subject: [PATCH 2/3] build(configs/): updates the config directory --- configs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs b/configs index 342c76d..16585d2 160000 --- a/configs +++ b/configs @@ -1 +1 @@ -Subproject commit 342c76d9c3125c8b6aa4b85e39f73d6dc6b01dd6 +Subproject commit 16585d2b5f9d67a0ced660b90a31a85be52d11ff From 133fd84c70b034cf6b7d199d5a3209b72fc24c14 Mon Sep 17 00:00:00 2001 From: Thibault Falque Date: Tue, 26 Aug 2025 16:06:18 +0200 Subject: [PATCH 3/3] .idea --- .idea/misc.xml | 1 + .idea/xcsprunner.iml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 73f1a8c..27e8af2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,4 +3,5 @@ + \ No newline at end of file diff --git a/.idea/xcsprunner.iml b/.idea/xcsprunner.iml index bdb36a8..c46990f 100644 --- a/.idea/xcsprunner.iml +++ b/.idea/xcsprunner.iml @@ -5,7 +5,7 @@ - + \ No newline at end of file