mirror of
https://github.com/logos-co/logos-monorepo.git
synced 2026-08-27 10:11:12 +00:00
* fix: pin logos-libp2p-module's libp2p input to a SHA (deleted upstream branch) The nightly bump's "Update flake.lock" step failed: error: unable to download 'https://api.github.com/repos/vacp2p/nim-libp2p/commits/feat/cbind/xpr-decode': HTTP error 422 logos-libp2p-module pins its `libp2p` input to the upstream branch `vacp2p/nim-libp2p/feat/cbind/xpr-decode`, which was deleted. Re-locking that input (nix flake lock --update-input logos-libp2p-module) re-resolves the branch ref and 422s, so the candidate is never staged. Override `libp2p` at the workspace level to the exact SHA the branch last pointed at (7c73484c — what the lock already resolved to; never merged to master, reachable only by SHA). Behavior-preserving. Because the nightly runs `ws sync-graph` BEFORE the lock update — which regenerates the auto-inputs block of flake.nix — a hand-edited override would be wiped every run. So the override is emitted by sync-graph itself via a new `get_manual_url_overrides` helper (mirrors `get_manual_follows`), and also applied to the committed flake.nix/flake.lock so master is self-consistent now. Stopgap until logos-libp2p-module SHA-pins libp2p in its own flake.nix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ws): filter and sort transitive URL overrides in sync-graph Address review feedback on get_manual_url_overrides emission: - Filter each override iname against the downstream flake's declared_set (same guard the follows path uses) so a renamed/removed upstream input can't leave a stale override that adds a spurious lock node; warn on skip. - Sort the kept overrides for deterministic, idempotent flake.nix output when a repo ever declares more than one override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
3828 lines
132 KiB
Bash
Executable File
3828 lines
132 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# ── Logos Workspace CLI ──────────────────────────────────────────────────────
|
|
# Unified interface for building, testing, and managing the multi-repo workspace.
|
|
|
|
WORKSPACE_ROOT="${LOGOS_WORKSPACE_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
REPOS_DIR="$WORKSPACE_ROOT/repos"
|
|
DEP_GRAPH="$WORKSPACE_ROOT/nix/dep-graph.nix"
|
|
|
|
# shellcheck source=scripts/_standalone-app
|
|
source "$WORKSPACE_ROOT/scripts/_standalone-app"
|
|
|
|
# Show help text and exit. Usage: show_help "text"
|
|
show_help() { echo -e "$1"; exit 0; }
|
|
# Check if any argument is --help or -h
|
|
wants_help() { for a in "$@"; do [[ "$a" == "--" ]] && return 1; [[ "$a" == "--help" || "$a" == "-h" ]] && return 0; done; return 1; }
|
|
|
|
# Global quiet flag — suppresses informational output
|
|
QUIET=false
|
|
# Global verbose flag — shows full nix output (default: capture and only show on error)
|
|
VERBOSE=false
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
NC='\033[0m'
|
|
|
|
# Logging: log() is suppressed in quiet mode; errors always print to stderr
|
|
log() { $QUIET || echo -e "$@"; }
|
|
|
|
# Run a nix command, capturing output unless --verbose is set.
|
|
# On failure, captured output is dumped to stderr.
|
|
# Usage: run_nix <nix-args...>
|
|
run_nix() {
|
|
if $VERBOSE; then
|
|
nix "$@"
|
|
else
|
|
local _nix_output _nix_rc=0
|
|
_nix_output=$(nix "$@" 2>&1) || _nix_rc=$?
|
|
if [[ $_nix_rc -ne 0 ]]; then
|
|
echo "$_nix_output" >&2
|
|
return $_nix_rc
|
|
fi
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
# Run a nix command for tests — stderr (warnings) is suppressed unless --verbose,
|
|
# but stdout (test output with -L) always flows through. On failure, captured stderr is shown.
|
|
run_nix_test() {
|
|
if $VERBOSE; then
|
|
nix "$@"
|
|
else
|
|
local _nix_stderr_file _nix_rc=0
|
|
_nix_stderr_file=$(mktemp)
|
|
nix "$@" 2>"$_nix_stderr_file" || _nix_rc=$?
|
|
if [[ $_nix_rc -ne 0 ]]; then
|
|
cat "$_nix_stderr_file" >&2
|
|
fi
|
|
rm -f "$_nix_stderr_file"
|
|
return $_nix_rc
|
|
fi
|
|
}
|
|
|
|
|
|
# ── Repo registry ───────────────────────────────────────────────────────────
|
|
# Format: "input_name|directory_name|git_url|has_flake"
|
|
# input_name: name in flake.nix inputs (empty string for non-flake repos)
|
|
# directory_name: submodule dir under repos/
|
|
# git_url: clone URL
|
|
# has_flake: "yes" or "no"
|
|
|
|
REPOS=(
|
|
# Shared Nix infrastructure
|
|
"logos-nix|logos-nix|https://github.com/logos-co/logos-nix.git|yes"
|
|
|
|
# Foundation
|
|
"logos-protocol|logos-protocol|https://github.com/logos-co/logos-protocol.git|yes"
|
|
"logos-qt-sdk|logos-qt-sdk|https://github.com/logos-co/logos-qt-sdk.git|yes"
|
|
"logos-cpp-sdk|logos-cpp-sdk|https://github.com/logos-co/logos-cpp-sdk.git|yes"
|
|
"logos-view-module-runtime|logos-view-module-runtime|https://github.com/logos-co/logos-view-module-runtime.git|yes"
|
|
"logos-module-client|logos-module-client|https://github.com/logos-co/logos-module-client.git|yes"
|
|
"logos-module|logos-module|https://github.com/logos-co/logos-module.git|yes"
|
|
"logos-liblogos|logos-liblogos|https://github.com/logos-co/logos-liblogos.git|yes"
|
|
"logos-logoscore-cli|logos-logoscore-cli|https://github.com/logos-co/logos-logoscore-cli.git|yes"
|
|
"logos-capability-module|logos-capability-module|https://github.com/logos-co/logos-capability-module.git|yes"
|
|
"logos-package|logos-package|https://github.com/logos-co/logos-package.git|yes"
|
|
"logos-plugin-qt|logos-plugin-qt|https://github.com/logos-co/logos-plugin-qt.git|yes"
|
|
"logos-module-builder|logos-module-builder|https://github.com/logos-co/logos-module-builder.git|yes"
|
|
|
|
# Packaging / Bundling
|
|
"nix-bundle-dir|nix-bundle-dir|https://github.com/logos-co/nix-bundle-dir.git|yes"
|
|
"nix-bundle-lgx|nix-bundle-lgx|https://github.com/logos-co/nix-bundle-lgx.git|yes"
|
|
"nix-bundle-appimage|nix-bundle-appimage|https://github.com/logos-co/nix-bundle-appimage.git|yes"
|
|
"nix-bundle-macos-app|nix-bundle-macos-app|https://github.com/logos-co/nix-bundle-macos-app.git|yes"
|
|
|
|
# App
|
|
"logos-standalone-app|logos-standalone-app|https://github.com/logos-co/logos-standalone-app.git|yes"
|
|
"logos-basecamp|logos-basecamp|https://github.com/logos-co/logos-basecamp.git|yes"
|
|
|
|
# Accounts
|
|
"logos-accounts-module|logos-accounts-module|https://github.com/logos-co/logos-accounts-module.git|yes"
|
|
"logos-accounts-ui|logos-accounts-ui|https://github.com/logos-co/logos-accounts-ui.git|yes"
|
|
|
|
# Chat & Messaging
|
|
"logos-chat-module|logos-chat-module|https://github.com/logos-co/logos-chat-module.git|yes"
|
|
"logos-chat-ui|logos-chat-ui|https://github.com/logos-co/logos-chat-ui.git|yes"
|
|
|
|
# Package Manager
|
|
"logos-package-manager|logos-package-manager|https://github.com/logos-co/logos-package-manager.git|yes"
|
|
"logos-package-downloader|logos-package-downloader|https://github.com/logos-co/logos-package-downloader.git|yes"
|
|
"logos-package-manager-module|logos-package-manager-module|https://github.com/logos-co/logos-package-manager-module.git|yes"
|
|
"logos-package-downloader-module|logos-package-downloader-module|https://github.com/logos-co/logos-package-downloader-module.git|yes"
|
|
"logos-package-manager-ui|logos-package-manager-ui|https://github.com/logos-co/logos-package-manager-ui.git|yes"
|
|
|
|
# Storage
|
|
"logos-storage-module|logos-storage-module|https://github.com/logos-co/logos-storage-module.git|yes"
|
|
"logos-storage-ui|logos-storage-ui|https://github.com/logos-co/logos-storage-ui.git|yes"
|
|
|
|
# Wallet
|
|
"logos-wallet-module|logos-wallet-module|https://github.com/logos-co/logos-wallet-module.git|yes"
|
|
"logos-wallet-ui|logos-wallet-ui|https://github.com/logos-co/logos-wallet-ui.git|yes"
|
|
|
|
# Delivery
|
|
"logos-delivery-module|logos-delivery-module|https://github.com/logos-co/logos-delivery-module.git|yes"
|
|
"logos-delivery-demo|logos-delivery-demo|https://github.com/logos-co/logos-delivery-demo.git|yes"
|
|
|
|
# Blockchain / Execution zone (LEX)
|
|
"logos-blockchain-module|logos-blockchain-module|https://github.com/logos-blockchain/logos-blockchain-module.git|yes"
|
|
"logos-blockchain-ui|logos-blockchain-ui|https://github.com/logos-blockchain/logos-blockchain-ui.git|yes"
|
|
"logos-execution-zone-wallet-ui|logos-execution-zone-wallet-ui|https://github.com/logos-blockchain/logos-execution-zone-wallet-ui.git|yes"
|
|
"logos-execution-zone-module|logos-execution-zone-module|https://github.com/logos-blockchain/logos-execution-zone-module.git|yes"
|
|
|
|
# Other modules
|
|
"logos-libp2p-module|logos-libp2p-module|https://github.com/logos-co/logos-libp2p-module.git|yes"
|
|
|
|
"logos-webview-app|logos-webview-app|https://github.com/logos-co/logos-webview-app.git|yes"
|
|
"counter_qml|counter_qml|https://github.com/logos-co/counter_qml.git|yes"
|
|
"counter|counter|https://github.com/logos-co/counter.git|yes"
|
|
|
|
# UI
|
|
"logos-design-system|logos-design-system|https://github.com/logos-co/logos-design-system.git|yes"
|
|
|
|
# SDKs
|
|
"logos-js-sdk|logos-js-sdk|https://github.com/logos-co/logos-js-sdk.git|yes"
|
|
"logos-nim-sdk|logos-nim-sdk|https://github.com/logos-co/logos-nim-sdk.git|yes"
|
|
"logos-rust-sdk|logos-rust-sdk|https://github.com/logos-co/logos-rust-sdk.git|yes"
|
|
"logos-logoscore-py|logos-logoscore-py|https://github.com/logos-co/logos-logoscore-py.git|yes"
|
|
|
|
# Testing
|
|
"logos-test-modules|logos-test-modules|https://github.com/logos-co/logos-test-modules.git|yes"
|
|
|
|
# Other
|
|
"process-stats|process-stats|https://github.com/logos-co/process-stats.git|yes"
|
|
"logos-module-viewer|logos-module-viewer|https://github.com/logos-co/logos-module-viewer.git|yes"
|
|
|
|
# EVM wallet (logos-evm-* modules + the fail-closed net-proxy crate).
|
|
# input_name == dir == GitHub slug (the doctest matrix in logos-doctest-hub
|
|
# intersects repos.json names against submodule dir names, so they must match).
|
|
"logos-evm-net-proxy|logos-evm-net-proxy|https://github.com/logos-co/logos-evm-net-proxy.git|yes"
|
|
"logos-evm-keystore-module|logos-evm-keystore-module|https://github.com/logos-co/logos-evm-keystore-module.git|yes"
|
|
"logos-evm-eth-rpc-module|logos-evm-eth-rpc-module|https://github.com/logos-co/logos-evm-eth-rpc-module.git|yes"
|
|
"logos-evm-token-list-module|logos-evm-token-list-module|https://github.com/logos-co/logos-evm-token-list-module.git|yes"
|
|
"logos-evm-uniswap-module|logos-evm-uniswap-module|https://github.com/logos-co/logos-evm-uniswap-module.git|yes"
|
|
"logos-evm-railgun-module|logos-evm-railgun-module|https://github.com/logos-co/logos-evm-railgun-module.git|yes"
|
|
"logos-evm-wallet-backend-module|logos-evm-wallet-backend-module|https://github.com/logos-co/logos-evm-wallet-backend-module.git|yes"
|
|
"logos-evm-wallet-ui|logos-evm-wallet-ui|https://github.com/logos-co/logos-evm-wallet-ui.git|yes"
|
|
|
|
# Non-flake repos (submodules only)
|
|
"|logos-template-module|https://github.com/logos-co/logos-template-module.git|no"
|
|
"|logos-docs|https://github.com/logos-co/logos-docs.git|no"
|
|
"|logos-website|https://github.com/logos-co/logos-website.git|no"
|
|
"|logos-tutorial|https://github.com/logos-co/logos-tutorial.git|no"
|
|
"|node-configs|https://github.com/logos-co/node-configs.git|no"
|
|
)
|
|
|
|
# ── Repo groups ──────────────────────────────────────────────────────────────
|
|
# Format: "group_name|member1,member2,..."
|
|
# Members can be repo names or other group names (resolved recursively).
|
|
|
|
REPO_GROUPS=(
|
|
"core|logos-protocol,logos-qt-sdk,logos-cpp-sdk,logos-module,logos-liblogos,logos-logoscore-cli,logos-module-builder,logos-capability-module,logos-package-manager-module,logos-test-modules"
|
|
"chat|core,logos-chat-module"
|
|
"package-manager|logos-package,logos-package-manager,logos-package-downloader,logos-package-manager-module,logos-package-downloader-module,logos-package-manager-ui,nix-bundle-lgx"
|
|
"app|core,logos-basecamp,logos-package-manager-ui"
|
|
"evm-wallet|logos-evm-net-proxy,logos-evm-keystore-module,logos-evm-eth-rpc-module,logos-evm-token-list-module,logos-evm-uniswap-module,logos-evm-railgun-module,logos-evm-wallet-backend-module,logos-evm-wallet-ui"
|
|
)
|
|
|
|
# ── Helper functions ─────────────────────────────────────────────────────────
|
|
|
|
get_field() {
|
|
echo "$1" | cut -d'|' -f"$2"
|
|
}
|
|
|
|
find_repo_entry() {
|
|
local query="$1"
|
|
for entry in "${REPOS[@]}"; do
|
|
local input_name dir_name
|
|
input_name=$(get_field "$entry" 1)
|
|
dir_name=$(get_field "$entry" 2)
|
|
if [[ "$dir_name" == "$query" || "$input_name" == "$query" ]]; then
|
|
echo "$entry"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
get_input_name() {
|
|
local query="$1"
|
|
for entry in "${REPOS[@]}"; do
|
|
if [[ "$(get_field "$entry" 2)" == "$query" || "$(get_field "$entry" 1)" == "$query" ]]; then
|
|
echo "$(get_field "$entry" 1)"
|
|
return 0
|
|
fi
|
|
done
|
|
echo "$query"
|
|
}
|
|
|
|
get_repo_path() {
|
|
local query="$1"
|
|
for entry in "${REPOS[@]}"; do
|
|
if [[ "$(get_field "$entry" 2)" == "$query" || "$(get_field "$entry" 1)" == "$query" ]]; then
|
|
echo "$(get_field "$entry" 2)"
|
|
return 0
|
|
fi
|
|
done
|
|
echo "$query"
|
|
}
|
|
|
|
# Check if a name is a defined group
|
|
is_group() {
|
|
local name="$1"
|
|
for entry in "${REPO_GROUPS[@]}"; do
|
|
if [[ "$(echo "$entry" | cut -d'|' -f1)" == "$name" ]]; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Expand a group name into a newline-separated list of repo names.
|
|
# Recursively resolves nested group references; deduplicates results.
|
|
expand_group() {
|
|
local name="$1"
|
|
local _visited="${2:-}"
|
|
|
|
# Cycle detection
|
|
if echo "$_visited" | grep -qw "$name" 2>/dev/null; then
|
|
return
|
|
fi
|
|
_visited="$_visited $name"
|
|
|
|
local found=false
|
|
for entry in "${REPO_GROUPS[@]}"; do
|
|
local gname members
|
|
gname=$(echo "$entry" | cut -d'|' -f1)
|
|
if [[ "$gname" == "$name" ]]; then
|
|
members=$(echo "$entry" | cut -d'|' -f2)
|
|
found=true
|
|
IFS=',' read -ra tokens <<< "$members"
|
|
for token in "${tokens[@]}"; do
|
|
if is_group "$token"; then
|
|
expand_group "$token" "$_visited"
|
|
else
|
|
echo "$token"
|
|
fi
|
|
done
|
|
break
|
|
fi
|
|
done
|
|
|
|
if ! $found; then
|
|
echo -e "${RED}Error:${NC} unknown group '$name'" >&2
|
|
echo "Available groups: $(printf '%s\n' "${REPO_GROUPS[@]}" | cut -d'|' -f1 | tr '\n' ' ')" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Get repos that are dirty (have local uncommitted changes)
|
|
get_dirty_repos() {
|
|
local dirty=()
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name
|
|
dir_name=$(get_field "$entry" 2)
|
|
local repo_path="$REPOS_DIR/$dir_name"
|
|
if [[ -d "$repo_path/.git" ]] || [[ -f "$repo_path/.git" ]]; then
|
|
if [[ -n "$(git -C "$repo_path" status --porcelain 2>/dev/null)" ]] || \
|
|
[[ -n "$(git -C "$repo_path" log '@{upstream}..HEAD' --oneline 2>/dev/null)" ]]; then
|
|
dirty+=("$dir_name")
|
|
fi
|
|
fi
|
|
done
|
|
echo ${dirty[@]+"${dirty[@]}"}
|
|
}
|
|
|
|
# Get all cloned repos that have flakes (for --workspace override)
|
|
get_all_flake_repos() {
|
|
local repos=()
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name has_flake
|
|
dir_name=$(get_field "$entry" 2)
|
|
has_flake=$(get_field "$entry" 4)
|
|
if [[ "$has_flake" == "yes" ]] && [[ -d "$REPOS_DIR/$dir_name" ]]; then
|
|
repos+=("$dir_name")
|
|
fi
|
|
done
|
|
echo "${repos[@]}"
|
|
}
|
|
|
|
# Build --override-input flags for dirty repos
|
|
build_override_flags() {
|
|
local flags=()
|
|
local explicit_overrides=("$@")
|
|
|
|
for dir_name in "${explicit_overrides[@]}"; do
|
|
local input_name repo_path
|
|
input_name=$(get_input_name "$dir_name")
|
|
repo_path=$(get_repo_path "$dir_name")
|
|
if [[ -n "$input_name" ]]; then
|
|
flags+=("--override-input" "$input_name" "path:$REPOS_DIR/$repo_path")
|
|
fi
|
|
done
|
|
echo "${flags[@]}"
|
|
}
|
|
|
|
# Check if a repo has tests according to dep-graph metadata
|
|
has_tests() {
|
|
local target="$1"
|
|
if [[ ! -f "$DEP_GRAPH" ]]; then
|
|
return 0 # assume yes if no metadata
|
|
fi
|
|
local result
|
|
result=$(nix eval --extra-experimental-features "nix-command flakes" \
|
|
--impure --raw --expr '
|
|
let graph = import '"$DEP_GRAPH"';
|
|
in if (graph.${"'"$target"'"} or null) == null then "unknown"
|
|
else if graph.${"'"$target"'"}.hasTests then "true"
|
|
else "false"
|
|
' 2>/dev/null)
|
|
[[ "$result" != "false" ]]
|
|
}
|
|
|
|
# Get dependents of a repo (repos that depend on it)
|
|
get_dependents() {
|
|
local target="$1"
|
|
if [[ ! -f "$DEP_GRAPH" ]]; then
|
|
return
|
|
fi
|
|
nix eval --extra-experimental-features "nix-command flakes" \
|
|
--impure --raw --expr '
|
|
let
|
|
graph = import '"$DEP_GRAPH"';
|
|
names = builtins.attrNames graph;
|
|
dependents = builtins.filter (n: builtins.elem "'"$target"'" graph.${n}.deps) names;
|
|
in builtins.concatStringsSep "\n" dependents
|
|
' 2>/dev/null
|
|
}
|
|
|
|
# Map a "owner/repo" GitHub identifier to the workspace flake input name.
|
|
_github_repo_to_input_name() {
|
|
local gh_repo="$1"
|
|
for entry in "${REPOS[@]}"; do
|
|
local input url
|
|
input=$(get_field "$entry" 1)
|
|
url=$(get_field "$entry" 3)
|
|
[[ -z "$input" ]] && continue
|
|
if [[ "$url" == "https://github.com/${gh_repo}.git" || \
|
|
"$url" == "https://github.com/${gh_repo}" ]]; then
|
|
echo "$input"
|
|
return 0
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Compute topological propagation levels starting from a given dependency.
|
|
# Uses level-based relaxation: each level is a batch of repos that can be
|
|
# updated in parallel, but all repos in a level must be committed and pushed
|
|
# to GitHub before the next level can run nix flake lock (nix fetches from
|
|
# remote URLs, not local paths).
|
|
#
|
|
# Usage: get_topo_levels <start_dep_input_name> [--for <target_input_name>]
|
|
#
|
|
# Output (one line per affected repo, sorted by level then name):
|
|
# LEVEL|REPO_INPUT_NAME|CHANGED_DEP1,CHANGED_DEP2,...
|
|
#
|
|
# --for <target>: only include repos on paths from start_dep to target
|
|
get_topo_levels() {
|
|
local start_dep="$1"
|
|
local for_target=""
|
|
shift
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--for) for_target="$2"; shift 2 ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
|
|
[[ ! -f "$DEP_GRAPH" ]] && return
|
|
|
|
local graph_json
|
|
graph_json=$(nix eval --extra-experimental-features "nix-command flakes" \
|
|
--impure --json --expr 'import '"$DEP_GRAPH"'' 2>/dev/null) || return
|
|
|
|
echo "$graph_json" | jq -r --arg start "$start_dep" --arg target "$for_target" '
|
|
. as $g |
|
|
|
|
# Compute propagation levels via Gauss-Seidel relaxation.
|
|
# Level 0 = start_dep (seed). Level n = repos whose deepest upstream
|
|
# changed dep is at level n-1. Uses max-of-paths for diamond deps.
|
|
( $g | keys_unsorted | map({key: ., value: -1}) | from_entries | .[$start] = 0 ) |
|
|
reduce range(60) as $_ (
|
|
.;
|
|
. as $acc |
|
|
reduce ($g | keys_unsorted[] | select(. != $start)) as $repo (
|
|
$acc;
|
|
. as $cur |
|
|
( [($g[$repo].deps // [])[] | $cur[.] // -1] |
|
|
if length > 0 then max else -1 end
|
|
) as $max_dep |
|
|
if $max_dep >= 0 and ($max_dep + 1) > $cur[$repo]
|
|
then .[$repo] = ($max_dep + 1)
|
|
else .
|
|
end
|
|
)
|
|
) | . as $levels |
|
|
|
|
# If --for given, compute the set of transitive deps of target.
|
|
# Output is restricted to repos that are in both the forward propagation
|
|
# from start_dep AND the ancestor set of target (i.e. on a path between them).
|
|
( if $target != "" then
|
|
( $g | keys_unsorted | map({key: ., value: false}) | from_entries |
|
|
.[$target] = true ) |
|
|
reduce range(60) as $_ (
|
|
.;
|
|
. as $anc |
|
|
reduce ($g | keys_unsorted[]) as $repo (
|
|
$anc;
|
|
if .[$repo] then
|
|
reduce ($g[$repo].deps // [])[] as $dep (.; .[$dep] = true)
|
|
else .
|
|
end
|
|
)
|
|
)
|
|
else null
|
|
end
|
|
) as $ancestors |
|
|
|
|
# Output: repos with level > 0, filtered, sorted by level then name
|
|
[ $levels | to_entries[] |
|
|
select(.value > 0) |
|
|
select( if $ancestors != null then ($ancestors[.key] // false) else true end ) |
|
|
.key as $repo | .value as $lvl |
|
|
[ ($g[$repo].deps // [])[] |
|
|
select(. == $start or (($levels[.] // -1) > 0))
|
|
] as $changed_deps |
|
|
"\($lvl)|\($repo)|\($changed_deps | join(","))"
|
|
] | sort[]
|
|
'
|
|
}
|
|
|
|
# ── Commands ─────────────────────────────────────────────────────────────────
|
|
|
|
cmd_init() {
|
|
wants_help "$@" && show_help "Usage: ws init [jobs]
|
|
|
|
Clone all submodules under repos/.
|
|
|
|
Arguments:
|
|
jobs Number of parallel clone jobs (default: 4)"
|
|
|
|
log "${BOLD}Initializing workspace submodules...${NC}"
|
|
local jobs="${1:-4}"
|
|
|
|
# Add submodules that don't exist yet
|
|
local count=0
|
|
local total=${#REPOS[@]}
|
|
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name git_url
|
|
dir_name=$(get_field "$entry" 2)
|
|
git_url=$(get_field "$entry" 3)
|
|
count=$((count + 1))
|
|
|
|
if [[ -d "$REPOS_DIR/$dir_name/.git" ]] || [[ -f "$REPOS_DIR/$dir_name/.git" ]]; then
|
|
log " [${count}/${total}] ${GREEN}exists${NC} $dir_name"
|
|
continue
|
|
fi
|
|
|
|
log " [${count}/${total}] ${YELLOW}cloning${NC} $dir_name ..."
|
|
git -C "$WORKSPACE_ROOT" submodule add --depth 1 "$git_url" "repos/$dir_name" 2>/dev/null || \
|
|
echo -e " ${RED}failed${NC} $dir_name (may already be in .gitmodules)"
|
|
done
|
|
|
|
log ""
|
|
log "${BOLD}Initializing and updating submodules...${NC}"
|
|
git -C "$WORKSPACE_ROOT" submodule update --init --depth 1 --jobs "$jobs"
|
|
|
|
log ""
|
|
log "${GREEN}Done.${NC} $(ls -d "$REPOS_DIR"/*/ 2>/dev/null | wc -l) repos under repos/"
|
|
}
|
|
|
|
cmd_build() {
|
|
wants_help "$@" && show_help "Usage: ws build <repo[#output]...|--group name> [options]
|
|
|
|
Build one or more repos via nix.
|
|
|
|
Use repo#output to build a specific flake output instead of the default.
|
|
For example: ws build logos-basecamp#portable
|
|
|
|
Options:
|
|
--group, -g <name...> Use a named repo group (see 'ws groups')
|
|
--auto-local, -a Auto-detect dirty repos and use as local overrides
|
|
--local, -l <repo1> <repo2> Use specific repos as local overrides
|
|
--workspace, -w Override all deps with local workspace repos
|
|
|
|
Examples:
|
|
ws build logos-basecamp
|
|
ws build logos-basecamp#portable
|
|
ws build logos-basecamp logos-liblogos
|
|
ws build --group core
|
|
ws build logos-basecamp --auto-local
|
|
ws build logos-basecamp --local logos-cpp-sdk logos-liblogos
|
|
ws build logos-basecamp#bin-bundle-dir --local nix-bundle-lgx logos-liblogos
|
|
ws build logos-basecamp --workspace"
|
|
|
|
local repos=()
|
|
local auto_local=false
|
|
local use_workspace=false
|
|
local local_overrides=()
|
|
local extra_args=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--group|-g)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local _gexp; _gexp=$(expand_group "$1") || exit 1
|
|
while IFS= read -r r; do [[ -n "$r" ]] && repos+=("$r"); done <<< "$_gexp"
|
|
shift
|
|
done ;;
|
|
--auto-local|--autolocal|-a)
|
|
auto_local=true; shift ;;
|
|
--workspace|-w)
|
|
use_workspace=true; shift ;;
|
|
--local|-l)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local_overrides+=("$1"); shift
|
|
done ;;
|
|
-*)
|
|
extra_args+=("$1"); shift ;;
|
|
*)
|
|
repos+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ${#repos[@]} -eq 0 ]]; then
|
|
echo -e "${RED}Error:${NC} no repo specified" >&2
|
|
echo "Usage: ws build <repo...> [options]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Override all workspace repos
|
|
if $use_workspace; then
|
|
for dir in $(get_all_flake_repos); do
|
|
local_overrides+=("$dir")
|
|
done
|
|
fi
|
|
|
|
# Auto-detect dirty repos
|
|
if $auto_local; then
|
|
for dir in $(get_dirty_repos); do
|
|
local name
|
|
name=$(get_input_name "$dir")
|
|
if [[ -n "$name" ]]; then
|
|
local_overrides+=("$dir")
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Build override flags
|
|
local flags=()
|
|
if [[ ${#local_overrides[@]} -gt 0 ]]; then
|
|
local unique_overrides
|
|
unique_overrides=($(printf '%s\n' "${local_overrides[@]}" | sort -u))
|
|
log "${CYAN}Local overrides:${NC}"
|
|
for dir in "${unique_overrides[@]}"; do
|
|
log " ${YELLOW}*${NC} $dir → path:$REPOS_DIR/$dir"
|
|
done
|
|
log ""
|
|
flags=($(build_override_flags "${unique_overrides[@]}"))
|
|
fi
|
|
|
|
if [[ ${#repos[@]} -eq 1 ]]; then
|
|
local repo_spec="${repos[0]}"
|
|
local repo_name="${repo_spec%%#*}"
|
|
local output_suffix=""
|
|
[[ "$repo_spec" == *"#"* ]] && output_suffix="${repo_spec#*#}"
|
|
local input_name
|
|
input_name=$(get_input_name "$repo_name")
|
|
if [[ -z "$input_name" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo_name' has no flake input (non-flake repo)" >&2
|
|
exit 1
|
|
fi
|
|
# All repo outputs are exposed through the workspace flake:
|
|
# default → .#<input_name>, non-default → .#<input_name>--<output>
|
|
local nix_ref
|
|
if [[ -n "$output_suffix" ]]; then
|
|
nix_ref="$WORKSPACE_ROOT#$input_name--$output_suffix"
|
|
else
|
|
nix_ref="$WORKSPACE_ROOT#$input_name"
|
|
fi
|
|
local display_name="$input_name"
|
|
[[ -n "$output_suffix" ]] && display_name="$input_name#$output_suffix"
|
|
log "${BOLD}Building ${CYAN}$display_name${NC}${BOLD}...${NC}"
|
|
if $VERBOSE; then
|
|
exec nix build \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$nix_ref" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
${extra_args[@]+"${extra_args[@]}"}
|
|
else
|
|
if run_nix build \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$nix_ref" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
${extra_args[@]+"${extra_args[@]}"}; then
|
|
echo -e "${GREEN}OK${NC} $display_name"
|
|
else
|
|
echo -e "${RED}FAIL${NC} $display_name"
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
local failed=0
|
|
for repo_spec in "${repos[@]}"; do
|
|
local repo_name="${repo_spec%%#*}"
|
|
local output_suffix=""
|
|
[[ "$repo_spec" == *"#"* ]] && output_suffix="${repo_spec#*#}"
|
|
local input_name
|
|
input_name=$(get_input_name "$repo_name")
|
|
if [[ -z "$input_name" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo_name' has no flake input (non-flake repo)" >&2
|
|
failed=$((failed + 1))
|
|
continue
|
|
fi
|
|
local nix_ref
|
|
if [[ -n "$output_suffix" ]]; then
|
|
nix_ref="$WORKSPACE_ROOT#$input_name--$output_suffix"
|
|
else
|
|
nix_ref="$WORKSPACE_ROOT#$input_name"
|
|
fi
|
|
local display_name="$input_name"
|
|
[[ -n "$output_suffix" ]] && display_name="$input_name#$output_suffix"
|
|
|
|
log "${BOLD}Building ${CYAN}$display_name${NC}${BOLD}...${NC}"
|
|
if ! run_nix build \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$nix_ref" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
${extra_args[@]+"${extra_args[@]}"}; then
|
|
echo -e "${RED}FAIL${NC} $display_name"
|
|
failed=$((failed + 1))
|
|
else
|
|
echo -e "${GREEN}OK${NC} $display_name"
|
|
fi
|
|
log ""
|
|
done
|
|
[[ $failed -eq 0 ]] || exit 1
|
|
}
|
|
|
|
cmd_bundle() {
|
|
wants_help "$@" && show_help "Usage: ws bundle <repo...|--group name> [options]
|
|
|
|
Bundle one or more repos into .lgx packages using nix-bundle-lgx.
|
|
|
|
Options:
|
|
--group, -g <name...> Use a named repo group (see 'ws groups')
|
|
--auto-local, -a Auto-detect dirty repos and use as local overrides
|
|
--local, -l <repo1> <repo2> Use specific repos as local overrides
|
|
--workspace, -w Override all deps with local workspace repos
|
|
--mode, -m <dev|portable|dual> Bundle mode (default: dev)
|
|
-o, --out-dir <path> Output directory for multi-repo bundles (default: result-bundle)
|
|
|
|
Examples:
|
|
ws bundle logos-chat-module
|
|
ws bundle logos-chat-module --mode portable
|
|
ws bundle logos-chat-module logos-storage-module --auto-local
|
|
ws bundle --group core --mode dual"
|
|
|
|
local repos=()
|
|
local auto_local=false
|
|
local use_workspace=false
|
|
local local_overrides=()
|
|
local extra_args=()
|
|
local bundle_mode="dev"
|
|
local out_dir="result-bundle"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--group|-g)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local _gexp; _gexp=$(expand_group "$1") || exit 1
|
|
while IFS= read -r r; do [[ -n "$r" ]] && repos+=("$r"); done <<< "$_gexp"
|
|
shift
|
|
done ;;
|
|
--auto-local|--autolocal|-a)
|
|
auto_local=true; shift ;;
|
|
--workspace|-w)
|
|
use_workspace=true; shift ;;
|
|
--local|-l)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local_overrides+=("$1"); shift
|
|
done ;;
|
|
--mode|-m)
|
|
shift
|
|
if [[ $# -eq 0 ]]; then
|
|
echo -e "${RED}Error:${NC} --mode requires an argument (dev, portable, dual)" >&2
|
|
exit 1
|
|
fi
|
|
bundle_mode="$1"
|
|
if [[ "$bundle_mode" != "dev" && "$bundle_mode" != "portable" && "$bundle_mode" != "dual" ]]; then
|
|
echo -e "${RED}Error:${NC} invalid mode '$bundle_mode' (must be dev, portable, or dual)" >&2
|
|
exit 1
|
|
fi
|
|
shift ;;
|
|
-o|--out-dir)
|
|
shift
|
|
if [[ $# -eq 0 ]]; then
|
|
echo -e "${RED}Error:${NC} --out-dir requires an argument" >&2
|
|
exit 1
|
|
fi
|
|
out_dir="$1"; shift ;;
|
|
-*)
|
|
extra_args+=("$1"); shift ;;
|
|
*)
|
|
repos+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ${#repos[@]} -eq 0 ]]; then
|
|
echo -e "${RED}Error:${NC} no repo specified" >&2
|
|
echo "Usage: ws bundle <repo...> [options]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Resolve bundler variant
|
|
local bundler_ref="path:$REPOS_DIR/nix-bundle-lgx"
|
|
case "$bundle_mode" in
|
|
dev) ;; # default bundler
|
|
portable) bundler_ref="$bundler_ref#portable" ;;
|
|
dual) bundler_ref="$bundler_ref#dual" ;;
|
|
esac
|
|
|
|
# Override all workspace repos
|
|
if $use_workspace; then
|
|
for dir in $(get_all_flake_repos); do
|
|
local_overrides+=("$dir")
|
|
done
|
|
fi
|
|
|
|
# Auto-detect dirty repos
|
|
if $auto_local; then
|
|
for dir in $(get_dirty_repos); do
|
|
local name
|
|
name=$(get_input_name "$dir")
|
|
[[ -n "$name" ]] && local_overrides+=("$dir")
|
|
done
|
|
fi
|
|
|
|
# Build override flags
|
|
local flags=()
|
|
if [[ ${#local_overrides[@]} -gt 0 ]]; then
|
|
local unique_overrides
|
|
unique_overrides=($(printf '%s\n' "${local_overrides[@]}" | sort -u))
|
|
log "${CYAN}Local overrides:${NC}"
|
|
for dir in "${unique_overrides[@]}"; do
|
|
log " ${YELLOW}*${NC} $dir → path:$REPOS_DIR/$dir"
|
|
done
|
|
log ""
|
|
flags=($(build_override_flags "${unique_overrides[@]}"))
|
|
fi
|
|
|
|
# Deduplicate repos
|
|
repos=($(printf '%s\n' "${repos[@]}" | awk '!seen[$0]++'))
|
|
|
|
# Resolve bundle target: prefer the --lib output (has drv.src with metadata.json
|
|
# needed by nix-bundle-lgx), fall back to the default package.
|
|
_bundle_target() {
|
|
local name="$1"
|
|
# Check if the --lib package exists in the workspace flake
|
|
if nix eval --extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT#${name}--lib" --apply 'x: true' &>/dev/null; then
|
|
echo "${name}--lib"
|
|
else
|
|
echo "$name"
|
|
fi
|
|
}
|
|
|
|
if [[ ${#repos[@]} -eq 1 ]]; then
|
|
local input_name
|
|
input_name=$(get_input_name "${repos[0]}")
|
|
if [[ -z "$input_name" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '${repos[0]}' has no flake input (non-flake repo)" >&2
|
|
exit 1
|
|
fi
|
|
local target
|
|
target=$(_bundle_target "$input_name")
|
|
log "${BOLD}Bundling ${CYAN}$input_name${NC}${BOLD} (mode: $bundle_mode)...${NC}"
|
|
if $VERBOSE; then
|
|
exec nix bundle \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
--bundler "$bundler_ref" \
|
|
-o result \
|
|
"$WORKSPACE_ROOT#$target" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
${extra_args[@]+"${extra_args[@]}"}
|
|
else
|
|
if run_nix bundle \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
--bundler "$bundler_ref" \
|
|
-o result \
|
|
"$WORKSPACE_ROOT#$target" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
${extra_args[@]+"${extra_args[@]}"}; then
|
|
echo -e "${GREEN}OK${NC} $input_name → result/"
|
|
else
|
|
echo -e "${RED}FAIL${NC} $input_name"
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
local failed=0
|
|
for repo in "${repos[@]}"; do
|
|
local input_name
|
|
input_name=$(get_input_name "$repo")
|
|
if [[ -z "$input_name" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo' has no flake input (non-flake repo)" >&2
|
|
failed=$((failed + 1))
|
|
continue
|
|
fi
|
|
|
|
local target
|
|
target=$(_bundle_target "$input_name")
|
|
log "${BOLD}Bundling ${CYAN}$input_name${NC}${BOLD} (mode: $bundle_mode)...${NC}"
|
|
if ! run_nix bundle \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
--bundler "$bundler_ref" \
|
|
-o "$out_dir/$input_name" \
|
|
"$WORKSPACE_ROOT#$target" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
${extra_args[@]+"${extra_args[@]}"}; then
|
|
echo -e "${RED}FAIL${NC} $input_name"
|
|
failed=$((failed + 1))
|
|
else
|
|
echo -e "${GREEN}OK${NC} $input_name → $out_dir/$input_name/"
|
|
fi
|
|
log ""
|
|
done
|
|
|
|
if [[ $failed -eq 0 ]]; then
|
|
log "${GREEN}All ${#repos[@]} repos bundled successfully.${NC}"
|
|
log "Output: $out_dir/"
|
|
fi
|
|
[[ $failed -eq 0 ]] || exit 1
|
|
}
|
|
|
|
cmd_run() {
|
|
wants_help "$@" && show_help "Usage: ws run <repo|--group name> [options] [-- <app-args>]
|
|
|
|
Build and run a repo via nix. Only one repo at a time.
|
|
|
|
Options:
|
|
--group, -g <name> Use a named repo group (see 'ws groups')
|
|
--auto-local, -a Auto-detect dirty repos and use as local overrides
|
|
--local, -l <repo1> <repo2> Use specific repos as local overrides
|
|
|
|
logos-standalone-app extras (passed after --):
|
|
<ui-plugin-repo> Repo name to bundle via nix-bundle-lgx and install
|
|
into ./plugins, or an existing .dylib / plugin dir path.
|
|
--modules, -M <repo>[,...] Backend module repo(s) to bundle and install into
|
|
./modules. logos-capability-module is always auto-included.
|
|
|
|
Examples:
|
|
ws run logos-basecamp
|
|
ws run logos-basecamp --auto-local
|
|
ws run logos-basecamp --local logos-cpp-sdk
|
|
ws run logos-standalone-app -- logos-accounts-ui --modules logos-accounts-module
|
|
ws run logos-standalone-app -- ./repos/logos-accounts-ui/result/lib/accounts_ui.dylib"
|
|
|
|
local repos=()
|
|
local auto_local=false
|
|
local local_overrides=()
|
|
local extra_args=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--)
|
|
shift; extra_args+=("$@"); break ;;
|
|
--group|-g)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local _gexp; _gexp=$(expand_group "$1") || exit 1
|
|
while IFS= read -r r; do [[ -n "$r" ]] && repos+=("$r"); done <<< "$_gexp"
|
|
shift
|
|
done ;;
|
|
--auto-local|--autolocal|-a) auto_local=true; shift ;;
|
|
--local|-l)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local_overrides+=("$1"); shift
|
|
done ;;
|
|
-*)
|
|
extra_args+=("$1"); shift ;;
|
|
*)
|
|
repos+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ${#repos[@]} -eq 0 ]]; then
|
|
echo -e "${RED}Error:${NC} no repo specified" >&2
|
|
echo "Usage: ws run <repo> [options]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ${#repos[@]} -gt 1 ]]; then
|
|
echo -e "${RED}Error:${NC} ws run only supports a single repo (got: ${repos[*]})" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local repo="${repos[0]}"
|
|
local input_name
|
|
input_name=$(get_input_name "$repo")
|
|
if [[ -z "$input_name" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo' has no flake input" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if $auto_local; then
|
|
for dir in $(get_dirty_repos); do
|
|
local name
|
|
name=$(get_input_name "$dir")
|
|
[[ -n "$name" ]] && local_overrides+=("$dir")
|
|
done
|
|
fi
|
|
|
|
local flags=()
|
|
if [[ ${#local_overrides[@]} -gt 0 ]]; then
|
|
local unique=($(printf '%s\n' "${local_overrides[@]}" | sort -u))
|
|
log "${CYAN}Local overrides:${NC}"
|
|
for dir in "${unique[@]}"; do
|
|
log " ${YELLOW}*${NC} $dir"
|
|
done
|
|
log ""
|
|
flags=($(build_override_flags "${unique[@]}"))
|
|
fi
|
|
|
|
log "${BOLD}Running ${CYAN}$input_name${NC}${BOLD}...${NC}"
|
|
|
|
# For app repos, build first and check for Qt conflicts before launching.
|
|
case "$input_name" in
|
|
logos-standalone-app)
|
|
cmd_run_standalone_app "$input_name"
|
|
;;
|
|
logos-basecamp)
|
|
local store_path
|
|
store_path=$(nix build --extra-experimental-features "nix-command flakes" \
|
|
--no-link --print-out-paths "$WORKSPACE_ROOT#$input_name" \
|
|
${flags[@]+"${flags[@]}"}) || exit $?
|
|
|
|
# Check closure for multiple Qt versions
|
|
local qt_versions
|
|
qt_versions=$(nix-store -qR "$store_path" 2>/dev/null | grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+' | sort -u || true)
|
|
local qt_count
|
|
qt_count=$(echo "$qt_versions" | grep -c . 2>/dev/null || true)
|
|
if [[ "$qt_count" -gt 1 ]]; then
|
|
echo -e "${YELLOW}WARNING: Multiple Qt versions found in closure:${NC}" >&2
|
|
echo "$qt_versions" | while read -r v; do echo -e " ${YELLOW}*${NC} $v" >&2; done
|
|
echo -e "${YELLOW}If the app crashes, run: ${BOLD}ws check-qt $repo${NC}" >&2
|
|
fi
|
|
|
|
# Check for stale local module cache
|
|
local app_qt
|
|
app_qt=$(nix-store -qR "$store_path" 2>/dev/null | grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+' | sort -u | tail -1 || true)
|
|
if [[ -n "$app_qt" ]]; then
|
|
local cache_dir
|
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
cache_dir="$HOME/Library/Application Support/LogosBasecampNix"
|
|
else
|
|
cache_dir="${XDG_DATA_HOME:-$HOME/.local/share}/LogosBasecampNix"
|
|
fi
|
|
if [[ -d "$cache_dir" ]]; then
|
|
local cached_qt
|
|
cached_qt=$(find "$cache_dir" -type f \( -name "*.dylib" -o -name "*.so" \) -print0 2>/dev/null \
|
|
| xargs -0 strings 2>/dev/null | grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+' | sort -u || true)
|
|
if [[ -n "$cached_qt" && "$cached_qt" != *"$app_qt"* ]]; then
|
|
echo -e "${RED}STALE MODULE CACHE: Local modules use $cached_qt but build uses $app_qt${NC}" >&2
|
|
echo -e "${YELLOW}Fix: rm -rf $(printf '%q' "$cache_dir")${NC}" >&2
|
|
echo -e "${YELLOW}Then run again.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Run the already-built output directly from the store.
|
|
# Always pass -- before extra_args so app flags (e.g. --help, --plugin)
|
|
# are not consumed by nix run itself.
|
|
exec nix run --extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT#$input_name" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
-- \
|
|
${extra_args[@]+"${extra_args[@]}"}
|
|
;;
|
|
*)
|
|
exec nix run \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT#$input_name" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
-- \
|
|
${extra_args[@]+"${extra_args[@]}"}
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cmd_develop() {
|
|
wants_help "$@" && show_help "Usage: ws develop [repo...|--group name] [options]
|
|
|
|
Enter a nix devShell. Without a repo, enters the workspace shell (zsh + tmux + tools).
|
|
With one or more repos, enters each repo's own devShell sequentially.
|
|
|
|
Options:
|
|
--group, -g <name...> Use a named repo group (see 'ws groups')
|
|
--fresh Force rebuild (clear cached shell environment)
|
|
--auto-local, -a Auto-detect dirty repos and use as local overrides
|
|
--local, -l <repo1> <repo2> Use specific repos as local overrides
|
|
|
|
Examples:
|
|
ws develop Enter the workspace devShell
|
|
ws develop --fresh Rebuild the workspace devShell from scratch
|
|
ws develop logos-cpp-sdk Enter logos-cpp-sdk's own devShell
|
|
ws develop --group core Enter each core repo's devShell sequentially
|
|
ws develop logos-cpp-sdk logos-liblogos Enter each devShell sequentially"
|
|
|
|
local repos=()
|
|
local auto_local=false
|
|
local fresh=false
|
|
local local_overrides=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--group|-g)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local _gexp; _gexp=$(expand_group "$1") || exit 1
|
|
while IFS= read -r r; do [[ -n "$r" ]] && repos+=("$r"); done <<< "$_gexp"
|
|
shift
|
|
done ;;
|
|
--auto-local|--autolocal|-a) auto_local=true; shift ;;
|
|
--fresh) fresh=true; shift ;;
|
|
--local|-l)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local_overrides+=("$1"); shift
|
|
done ;;
|
|
-*) shift ;;
|
|
*) repos+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
if $auto_local; then
|
|
for dir in $(get_dirty_repos); do
|
|
local name
|
|
name=$(get_input_name "$dir")
|
|
[[ -n "$name" ]] && local_overrides+=("$dir")
|
|
done
|
|
fi
|
|
|
|
local flags=()
|
|
if [[ ${#local_overrides[@]} -gt 0 ]]; then
|
|
local unique=($(printf '%s\n' "${local_overrides[@]}" | sort -u))
|
|
flags=($(build_override_flags "${unique[@]}"))
|
|
fi
|
|
|
|
if [[ ${#repos[@]} -eq 0 ]]; then
|
|
# Workspace devShell
|
|
# Use cached dev environment to skip slow nix flake evaluation.
|
|
# Cache is invalidated when devshell.nix, flake.nix, or flake.lock change.
|
|
# Use --fresh to force rebuild.
|
|
local cache_dir="$WORKSPACE_ROOT/.dev-shell-cache"
|
|
local cache_env="$cache_dir/dev-env.sh"
|
|
local cache_hash_file="$cache_dir/hash"
|
|
|
|
if $fresh; then
|
|
rm -rf "$cache_dir"
|
|
fi
|
|
|
|
local flake_hash=""
|
|
if command -v shasum &>/dev/null; then
|
|
flake_hash=$(cat "$WORKSPACE_ROOT/nix/devshell.nix" "$WORKSPACE_ROOT/flake.nix" "$WORKSPACE_ROOT/flake.lock" 2>/dev/null | shasum | cut -d' ' -f1)
|
|
fi
|
|
|
|
# Check cache validity
|
|
local use_cache=false
|
|
if [[ ${#flags[@]} -eq 0 && -n "$flake_hash" && -f "$cache_hash_file" && -f "$cache_env" ]]; then
|
|
local cached_hash
|
|
cached_hash=$(cat "$cache_hash_file" 2>/dev/null)
|
|
[[ "$cached_hash" == "$flake_hash" ]] && use_cache=true
|
|
fi
|
|
|
|
if $use_cache; then
|
|
# Fast path: source cached env + run shellHook (sub-second)
|
|
set +eu
|
|
source "$cache_env"
|
|
eval "$shellHook"
|
|
return 0
|
|
fi
|
|
|
|
# Slow path: full nix evaluation
|
|
# Cache the result in background for next time
|
|
if [[ ${#flags[@]} -eq 0 && -n "$flake_hash" ]]; then
|
|
mkdir -p "$cache_dir"
|
|
echo ".dev-shell-cache/" >> "$WORKSPACE_ROOT/.gitignore" 2>/dev/null || true
|
|
nix print-dev-env \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT" > "$cache_env.tmp" 2>/dev/null && \
|
|
mv "$cache_env.tmp" "$cache_env" && \
|
|
echo "$flake_hash" > "$cache_hash_file" &
|
|
fi
|
|
|
|
exec nix develop \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT" \
|
|
${flags[@]+"${flags[@]}"}
|
|
elif [[ ${#repos[@]} -eq 1 ]]; then
|
|
local repo_path="$REPOS_DIR/${repos[0]}"
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '${repos[0]}' not found at $repo_path" >&2
|
|
exit 1
|
|
fi
|
|
exec nix develop \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"path:$repo_path" \
|
|
${flags[@]+"${flags[@]}"}
|
|
else
|
|
for repo in "${repos[@]}"; do
|
|
local repo_path="$REPOS_DIR/$repo"
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo' not found at $repo_path" >&2
|
|
continue
|
|
fi
|
|
log "${BOLD}Entering devShell for ${CYAN}$repo${NC}${BOLD}...${NC}"
|
|
nix develop \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"path:$repo_path" \
|
|
${flags[@]+"${flags[@]}"}
|
|
done
|
|
fi
|
|
}
|
|
|
|
_detect_system() {
|
|
nix eval --extra-experimental-features "nix-command flakes" \
|
|
--impure --raw --expr builtins.currentSystem 2>/dev/null || echo "x86_64-linux"
|
|
}
|
|
|
|
# Parse a JSON array string like ["a","b","c"] into newline-separated values (no jq needed)
|
|
_json_array_to_lines() {
|
|
echo "$1" | tr -d '[]"' | tr ',' '\n' | sed '/^$/d'
|
|
}
|
|
|
|
# Discover checks and test-like packages via nix eval.
|
|
# When override flags are present, uses installable syntax (flake ref + attribute)
|
|
# so --override-input flags are respected (builtins.getFlake ignores them).
|
|
_discover_test_outputs() {
|
|
local repo_path="$1"
|
|
local system="$2"
|
|
local override_flags="${3:-}"
|
|
|
|
if [[ -n "$override_flags" ]]; then
|
|
# Two evals with explicit attribute paths — --override-input applies to the flake ref
|
|
local checks_json test_pkgs_json
|
|
|
|
# shellcheck disable=SC2086
|
|
checks_json=$(nix eval --extra-experimental-features "nix-command flakes" \
|
|
--json "path:${repo_path}#checks.${system}" \
|
|
--apply 'builtins.attrNames' \
|
|
$override_flags 2>/dev/null) || checks_json='[]'
|
|
|
|
# shellcheck disable=SC2086
|
|
test_pkgs_json=$(nix eval --extra-experimental-features "nix-command flakes" \
|
|
--json "path:${repo_path}#packages.${system}" \
|
|
--apply 'ps: builtins.filter (n: builtins.match ".*[Tt]est.*" n != null) (builtins.attrNames ps)' \
|
|
$override_flags 2>/dev/null) || test_pkgs_json='[]'
|
|
|
|
echo "{\"checks\":${checks_json},\"testPkgs\":${test_pkgs_json}}"
|
|
else
|
|
# No overrides: single eval via builtins.getFlake (faster, no extra resolution)
|
|
nix eval --extra-experimental-features "nix-command flakes" \
|
|
--impure --raw --expr '
|
|
let
|
|
flake = builtins.getFlake "path:'"$repo_path"'";
|
|
checks = builtins.attrNames (flake.checks.'"$system"' or {});
|
|
packages = builtins.attrNames (flake.packages.'"$system"' or {});
|
|
testPkgs = builtins.filter (n: builtins.match ".*[Tt]est.*" n != null) packages;
|
|
in builtins.toJSON { inherit checks testPkgs; }
|
|
' 2>/dev/null || echo '{"checks":[],"testPkgs":[]}'
|
|
fi
|
|
}
|
|
|
|
# Run test for a single repo.
|
|
# Priority:
|
|
# 1. checks.<system>.tests (convention: runs tests)
|
|
# 2. Other checks.<system>.* (also run tests)
|
|
# 3. packages.<system>.*test* (test packages — build only)
|
|
# 4. Skip
|
|
_test_one_repo() {
|
|
local input_name="$1"
|
|
local dir_name="$2"
|
|
local system="$3"
|
|
local override_flags="${4:-}"
|
|
local use_workspace_flake="${5:-false}"
|
|
local repo_path="$REPOS_DIR/$dir_name"
|
|
|
|
log "${BOLD}Testing ${CYAN}$dir_name${NC}${BOLD}...${NC}"
|
|
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e " ${RED}not cloned${NC}"
|
|
return 1
|
|
fi
|
|
|
|
# Fast path: skip repos known to have no tests
|
|
local skip_known=false
|
|
has_tests "$input_name" || skip_known=true
|
|
if $skip_known; then
|
|
log " ${YELLOW}SKIP${NC} no tests (per nix/dep-graph.nix)"
|
|
return 2
|
|
fi
|
|
|
|
# Discover checks + test packages in one eval
|
|
log " ${BLUE}Discovering test outputs...${NC}"
|
|
local outputs
|
|
outputs=$(_discover_test_outputs "$repo_path" "$system" "$override_flags")
|
|
|
|
local checks_json test_pkgs_json
|
|
checks_json=$(echo "$outputs" | sed 's/.*"checks":\([^,}]*\).*/\1/')
|
|
test_pkgs_json=$(echo "$outputs" | sed 's/.*"testPkgs":\([^}]*\).*/\1/')
|
|
|
|
# Build the flake ref for checks and packages.
|
|
# When overrides are active (--local, --auto-local, --workspace), build from the
|
|
# workspace flake so that follows declarations propagate overrides to transitive deps.
|
|
# Without overrides, build from the individual repo flake (matches standalone nix flake check).
|
|
local check_ref pkg_ref
|
|
if [[ "$use_workspace_flake" == "true" ]]; then
|
|
# Workspace flake prefixes check names: checks.<system>.<input_name>--<check>
|
|
check_ref="$WORKSPACE_ROOT#checks.$system.${input_name}--"
|
|
pkg_ref="$WORKSPACE_ROOT#${input_name}--"
|
|
else
|
|
check_ref="path:$repo_path#checks.$system."
|
|
pkg_ref="path:$repo_path#"
|
|
fi
|
|
|
|
local ran_something=false
|
|
local all_pass=true
|
|
|
|
# 1. Run checks (convention: checks.*.tests first, then others)
|
|
local checks
|
|
checks=$(_json_array_to_lines "$checks_json")
|
|
|
|
if [[ -n "$checks" ]]; then
|
|
# If "tests" exists, run it first (convention)
|
|
if echo "$checks" | grep -qx "tests"; then
|
|
ran_something=true
|
|
log " ${YELLOW}check${NC} tests"
|
|
# shellcheck disable=SC2086
|
|
if run_nix_test build --extra-experimental-features "nix-command flakes" \
|
|
"${check_ref}tests" $override_flags -L; then
|
|
echo -e " ${GREEN}PASS${NC} ${dir_name} tests"
|
|
else
|
|
echo -e " ${RED}FAIL${NC} ${dir_name} tests"
|
|
all_pass=false
|
|
fi
|
|
fi
|
|
|
|
# Run any other checks
|
|
while IFS= read -r check; do
|
|
[[ -z "$check" || "$check" == "tests" ]] && continue
|
|
ran_something=true
|
|
log " ${YELLOW}check${NC} $check"
|
|
# shellcheck disable=SC2086
|
|
if run_nix_test build --extra-experimental-features "nix-command flakes" \
|
|
"${check_ref}${check}" $override_flags -L; then
|
|
echo -e " ${GREEN}PASS${NC} ${dir_name} $check"
|
|
else
|
|
echo -e " ${RED}FAIL${NC} ${dir_name} $check"
|
|
all_pass=false
|
|
fi
|
|
done <<< "$checks"
|
|
fi
|
|
|
|
# 2. If no checks, try test-like packages (build only)
|
|
if ! $ran_something; then
|
|
while IFS= read -r pkg; do
|
|
[[ -z "$pkg" ]] && continue
|
|
ran_something=true
|
|
log " ${YELLOW}package${NC} $pkg (build only — add checks.*.tests to run)"
|
|
# shellcheck disable=SC2086
|
|
if run_nix_test build --extra-experimental-features "nix-command flakes" \
|
|
"${pkg_ref}${pkg}" $override_flags -L; then
|
|
echo -e " ${GREEN}PASS${NC} ${dir_name} $pkg"
|
|
else
|
|
echo -e " ${RED}FAIL${NC} ${dir_name} $pkg"
|
|
all_pass=false
|
|
fi
|
|
done <<< "$(_json_array_to_lines "$test_pkgs_json")"
|
|
fi
|
|
|
|
# 3. Nothing found despite hasTests=true — metadata may be stale
|
|
if ! $ran_something; then
|
|
log " ${YELLOW}SKIP${NC} hasTests=true in dep-graph.nix but no checks/test packages found"
|
|
return 2
|
|
fi
|
|
|
|
$all_pass
|
|
}
|
|
|
|
cmd_test() {
|
|
wants_help "$@" && show_help "Usage: ws test [repo...|--group name] [options]
|
|
|
|
Run checks/tests for one or more repos, or all repos.
|
|
|
|
Options:
|
|
--group, -g <name...> Use a named repo group (see 'ws groups')
|
|
--all Test all repos (default if no repo given)
|
|
--type, -t <type> Filter by project type: cpp, rust, nim, js, qml
|
|
--auto-local, -a Auto-detect dirty repos and use as local overrides
|
|
--local, -l <repo1> <repo2> Use specific repos as local overrides
|
|
--workspace, -w Override all deps with local workspace repos
|
|
|
|
Examples:
|
|
ws test logos-module Test a single repo
|
|
ws test logos-module logos-liblogos Test multiple repos
|
|
ws test --group core Test all repos in the core group
|
|
ws test --all Test all repos
|
|
ws test --type cpp Test only C++ repos
|
|
ws test logos-module --type cpp
|
|
ws test logos-test-modules --auto-local
|
|
ws test logos-test-modules --local logos-liblogos
|
|
ws test --all --workspace Test all repos using workspace dep versions"
|
|
|
|
local targets=()
|
|
local test_all=false
|
|
local test_type=""
|
|
local auto_local=false
|
|
local use_workspace=false
|
|
local local_overrides=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--group|-g)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local _gexp; _gexp=$(expand_group "$1") || exit 1
|
|
while IFS= read -r r; do [[ -n "$r" ]] && targets+=("$r"); done <<< "$_gexp"
|
|
shift
|
|
done ;;
|
|
--type|-t) test_type="$2"; shift 2 ;;
|
|
--all) test_all=true; shift ;;
|
|
--auto-local|--autolocal|-a) auto_local=true; shift ;;
|
|
--workspace|-w) use_workspace=true; shift ;;
|
|
--local|-l)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local_overrides+=("$1"); shift
|
|
done ;;
|
|
-*) echo -e "${YELLOW}WARN${NC} Unknown flag: $1" >&2; shift ;;
|
|
*)
|
|
targets+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
# Override all workspace repos
|
|
if $use_workspace; then
|
|
for dir in $(get_all_flake_repos); do
|
|
local_overrides+=("$dir")
|
|
done
|
|
fi
|
|
|
|
# Auto-detect dirty repos
|
|
if $auto_local; then
|
|
for dir in $(get_dirty_repos); do
|
|
local name
|
|
name=$(get_input_name "$dir")
|
|
if [[ -n "$name" ]]; then
|
|
local_overrides+=("$dir")
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Build override flags
|
|
local override_flags=""
|
|
if [[ ${#local_overrides[@]} -gt 0 ]]; then
|
|
local unique_overrides
|
|
unique_overrides=($(printf '%s\n' "${local_overrides[@]}" | sort -u))
|
|
log "${CYAN}Local overrides:${NC}"
|
|
for dir in "${unique_overrides[@]}"; do
|
|
log " ${YELLOW}*${NC} $dir → path:$REPOS_DIR/$dir"
|
|
done
|
|
log ""
|
|
override_flags=$(build_override_flags "${unique_overrides[@]}")
|
|
fi
|
|
|
|
# Use workspace flake when overrides are active so follows propagate to transitive deps
|
|
local use_workspace_flake=false
|
|
if [[ -n "$override_flags" ]]; then
|
|
use_workspace_flake=true
|
|
fi
|
|
|
|
local system
|
|
system=$(_detect_system)
|
|
|
|
if $test_all || [[ ${#targets[@]} -eq 0 ]]; then
|
|
log "${BOLD}Running tests across all repos (system: $system)${NC}"
|
|
log ""
|
|
|
|
local passed=0 failed=0 skipped=0
|
|
local passed_repos="" failed_repos=""
|
|
|
|
for entry in "${REPOS[@]}"; do
|
|
local input_name dir_name has_flake
|
|
input_name=$(get_field "$entry" 1)
|
|
dir_name=$(get_field "$entry" 2)
|
|
has_flake=$(get_field "$entry" 4)
|
|
|
|
if [[ "$has_flake" != "yes" || -z "$input_name" ]]; then
|
|
skipped=$((skipped + 1))
|
|
continue
|
|
fi
|
|
|
|
# Filter by type if specified
|
|
if [[ -n "$test_type" ]]; then
|
|
local repo_path="$REPOS_DIR/$dir_name"
|
|
local matches=false
|
|
case "$test_type" in
|
|
cpp|cmake)
|
|
[[ -f "$repo_path/CMakeLists.txt" ]] && matches=true ;;
|
|
rust|cargo)
|
|
[[ -f "$repo_path/Cargo.toml" ]] && matches=true ;;
|
|
nim|nimble)
|
|
ls "$repo_path"/*.nimble >/dev/null 2>&1 && matches=true ;;
|
|
js|node|npm)
|
|
[[ -f "$repo_path/package.json" ]] && matches=true ;;
|
|
qml)
|
|
ls "$repo_path"/*.qml >/dev/null 2>&1 && matches=true
|
|
[[ -f "$repo_path/CMakeLists.txt" ]] && grep -q "qml" "$repo_path/CMakeLists.txt" 2>/dev/null && matches=true ;;
|
|
*)
|
|
echo -e "${RED}Unknown type: $test_type${NC}" >&2
|
|
echo "Available types: cpp, rust, nim, js, qml" >&2
|
|
exit 1 ;;
|
|
esac
|
|
if ! $matches; then
|
|
skipped=$((skipped + 1))
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
local rc=0
|
|
_test_one_repo "$input_name" "$dir_name" "$system" "$override_flags" "$use_workspace_flake" || rc=$?
|
|
if [[ $rc -eq 0 ]]; then
|
|
passed=$((passed + 1))
|
|
passed_repos="$passed_repos $dir_name"
|
|
elif [[ $rc -eq 2 ]]; then
|
|
skipped=$((skipped + 1))
|
|
else
|
|
failed=$((failed + 1))
|
|
failed_repos="$failed_repos $dir_name"
|
|
fi
|
|
log ""
|
|
done
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Results:${NC} ${GREEN}$passed passed${NC} ${RED}$failed failed${NC} ${YELLOW}$skipped skipped${NC}"
|
|
if [[ -n "$passed_repos" ]]; then
|
|
echo -e " ${GREEN}Passed:${NC}$passed_repos"
|
|
fi
|
|
if [[ -n "$failed_repos" ]]; then
|
|
echo -e " ${RED}Failed:${NC}$failed_repos"
|
|
fi
|
|
[[ $failed -eq 0 ]] || exit 1
|
|
elif [[ ${#targets[@]} -eq 1 ]]; then
|
|
local input_name dir_name
|
|
input_name=$(get_input_name "${targets[0]}")
|
|
dir_name="${targets[0]}"
|
|
|
|
if [[ -z "$input_name" ]]; then
|
|
echo -e "${RED}Error:${NC} '${targets[0]}' has no flake input" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local rc=0
|
|
_test_one_repo "$input_name" "$dir_name" "$system" "$override_flags" "$use_workspace_flake" || rc=$?
|
|
if [[ $rc -eq 1 ]]; then exit 1; fi
|
|
else
|
|
# Multiple repos
|
|
local passed=0 failed=0 skipped=0
|
|
local passed_repos="" failed_repos=""
|
|
|
|
for target in "${targets[@]}"; do
|
|
local input_name dir_name
|
|
input_name=$(get_input_name "$target")
|
|
dir_name="$target"
|
|
|
|
if [[ -z "$input_name" ]]; then
|
|
echo -e "${RED}Error:${NC} '$target' has no flake input" >&2
|
|
failed=$((failed + 1))
|
|
failed_repos="$failed_repos $target"
|
|
continue
|
|
fi
|
|
|
|
local rc=0
|
|
_test_one_repo "$input_name" "$dir_name" "$system" "$override_flags" "$use_workspace_flake" || rc=$?
|
|
if [[ $rc -eq 0 ]]; then
|
|
passed=$((passed + 1))
|
|
passed_repos="$passed_repos $dir_name"
|
|
elif [[ $rc -eq 2 ]]; then
|
|
skipped=$((skipped + 1))
|
|
else
|
|
failed=$((failed + 1))
|
|
failed_repos="$failed_repos $dir_name"
|
|
fi
|
|
log ""
|
|
done
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Results:${NC} ${GREEN}$passed passed${NC} ${RED}$failed failed${NC} ${YELLOW}$skipped skipped${NC}"
|
|
if [[ -n "$passed_repos" ]]; then
|
|
echo -e " ${GREEN}Passed:${NC}$passed_repos"
|
|
fi
|
|
if [[ -n "$failed_repos" ]]; then
|
|
echo -e " ${RED}Failed:${NC}$failed_repos"
|
|
fi
|
|
[[ $failed -eq 0 ]] || exit 1
|
|
fi
|
|
}
|
|
|
|
cmd_status() {
|
|
wants_help "$@" && show_help "Usage: ws status
|
|
|
|
Show git status across all repos — branch, dirty/clean, ahead/behind.
|
|
|
|
Examples:
|
|
ws status"
|
|
|
|
log "${BOLD}Workspace status${NC}"
|
|
log ""
|
|
|
|
local dirty=0 clean=0 missing=0
|
|
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name has_flake
|
|
dir_name=$(get_field "$entry" 2)
|
|
has_flake=$(get_field "$entry" 4)
|
|
local repo_path="$REPOS_DIR/$dir_name"
|
|
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e " ${RED}missing${NC} $dir_name"
|
|
missing=$((missing + 1))
|
|
continue
|
|
fi
|
|
|
|
if [[ ! -d "$repo_path/.git" && ! -f "$repo_path/.git" ]]; then
|
|
echo -e " ${RED}no-git${NC} $dir_name"
|
|
missing=$((missing + 1))
|
|
continue
|
|
fi
|
|
|
|
local status
|
|
status=$(git -C "$repo_path" status --porcelain 2>/dev/null)
|
|
local branch
|
|
branch=$(git -C "$repo_path" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "???")
|
|
local ahead_behind
|
|
ahead_behind=$(git -C "$repo_path" rev-list --left-right --count '@{upstream}...HEAD' 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$status" ]]; then
|
|
local modified added untracked
|
|
modified=$(echo "$status" | grep -c '^ M\| ^M' 2>/dev/null || echo 0)
|
|
added=$(echo "$status" | grep -c '^A ' 2>/dev/null || echo 0)
|
|
untracked=$(echo "$status" | grep -c '^??' 2>/dev/null || echo 0)
|
|
echo -e " ${YELLOW}dirty${NC} $dir_name ${BLUE}($branch)${NC} [M:$modified A:$added ?:$untracked]"
|
|
dirty=$((dirty + 1))
|
|
else
|
|
local marker="${GREEN}clean${NC}"
|
|
if [[ -n "$ahead_behind" ]]; then
|
|
local behind ahead
|
|
behind=$(echo "$ahead_behind" | awk '{print $1}')
|
|
ahead=$(echo "$ahead_behind" | awk '{print $2}')
|
|
if [[ "$ahead" -gt 0 ]]; then
|
|
marker="${GREEN}clean${NC} ${CYAN}+${ahead}${NC}"
|
|
fi
|
|
fi
|
|
echo -e " $marker $dir_name ${BLUE}($branch)${NC}"
|
|
clean=$((clean + 1))
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo -e " ${GREEN}$clean clean${NC} ${YELLOW}$dirty dirty${NC} ${RED}$missing missing${NC}"
|
|
}
|
|
|
|
cmd_dirty() {
|
|
wants_help "$@" && show_help "Usage: ws dirty
|
|
|
|
Show repos with uncommitted changes and what downstream repos they affect.
|
|
|
|
Examples:
|
|
ws dirty"
|
|
|
|
log "${BOLD}Repos with local changes:${NC}"
|
|
log ""
|
|
|
|
local dirty_repos
|
|
dirty_repos=($(get_dirty_repos))
|
|
|
|
if [[ ${#dirty_repos[@]} -eq 0 ]]; then
|
|
echo -e " ${GREEN}All repos are clean.${NC}"
|
|
return
|
|
fi
|
|
|
|
for dir_name in "${dirty_repos[@]}"; do
|
|
local input_name
|
|
input_name=$(get_input_name "$dir_name")
|
|
echo -e " ${YELLOW}*${NC} ${BOLD}$dir_name${NC}"
|
|
|
|
# Show what would be affected
|
|
local dependents
|
|
dependents=$(get_dependents "${input_name:-$dir_name}")
|
|
if [[ -n "$dependents" ]]; then
|
|
echo -e " affects: $(echo "$dependents" | tr '\n' ' ')"
|
|
fi
|
|
done
|
|
|
|
log ""
|
|
log "Use ${CYAN}ws build <repo> --auto-local${NC} to build with these overrides."
|
|
}
|
|
|
|
cmd_graph() {
|
|
wants_help "$@" && show_help "Usage: ws graph [repo...|--group name]
|
|
|
|
Show the dependency graph. With repos, shows deps and dependents for each.
|
|
Without a repo, outputs the full graph in DOT format.
|
|
|
|
Options:
|
|
--group, -g <name...> Use a named repo group (see 'ws groups')
|
|
|
|
Examples:
|
|
ws graph logos-basecamp Show deps and dependents of a repo
|
|
ws graph logos-basecamp logos-liblogos Show deps for multiple repos
|
|
ws graph --group core Show deps for all core repos
|
|
ws graph Output full DOT graph
|
|
ws graph | dot -Tsvg > g.svg Render as SVG (requires graphviz)"
|
|
|
|
local targets=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--group|-g)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local _gexp; _gexp=$(expand_group "$1") || exit 1
|
|
while IFS= read -r r; do [[ -n "$r" ]] && targets+=("$r"); done <<< "$_gexp"
|
|
shift
|
|
done ;;
|
|
-*) shift ;;
|
|
*) targets+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ! -f "$DEP_GRAPH" ]]; then
|
|
echo -e "${RED}Error:${NC} dep-graph.nix not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ${#targets[@]} -gt 0 ]]; then
|
|
for target in "${targets[@]}"; do
|
|
log "${BOLD}Dependencies of ${CYAN}$target${NC}${BOLD}:${NC}"
|
|
nix eval --extra-experimental-features "nix-command flakes" --impure --raw --expr '
|
|
let graph = import '"$DEP_GRAPH"';
|
|
in builtins.concatStringsSep "\n" ((graph.${"'"$target"'"} or { deps = []; }).deps or [])
|
|
' 2>/dev/null | while IFS= read -r dep || [[ -n "$dep" ]]; do
|
|
[[ -n "$dep" ]] && echo -e " -> $dep"
|
|
done
|
|
|
|
log ""
|
|
log "${BOLD}Dependents (repos that depend on ${CYAN}$target${NC}${BOLD}):${NC}"
|
|
nix eval --extra-experimental-features "nix-command flakes" --impure --raw --expr '
|
|
let
|
|
graph = import '"$DEP_GRAPH"';
|
|
names = builtins.attrNames graph;
|
|
dependents = builtins.filter (n: builtins.elem "'"$target"'" graph.${n}.deps) names;
|
|
in builtins.concatStringsSep "\n" dependents
|
|
' 2>/dev/null | while IFS= read -r dep || [[ -n "$dep" ]]; do
|
|
[[ -n "$dep" ]] && echo -e " <- $dep"
|
|
done
|
|
|
|
if [[ ${#targets[@]} -gt 1 ]]; then
|
|
log ""
|
|
fi
|
|
done
|
|
else
|
|
# Full graph in DOT format
|
|
nix eval --extra-experimental-features "nix-command flakes" --impure --raw --expr '
|
|
let
|
|
graph = import '"$DEP_GRAPH"';
|
|
names = builtins.attrNames graph;
|
|
edges = builtins.concatMap (k:
|
|
map (dep: " \"" + dep + "\" -> \"" + k + "\";") graph.${k}.deps
|
|
) names;
|
|
in "digraph logos {\n rankdir=BT;\n node [shape=box, fontsize=10];\n\n"
|
|
+ builtins.concatStringsSep "\n" edges
|
|
+ "\n}"
|
|
' 2>/dev/null
|
|
echo ""
|
|
$QUIET || echo -e "${CYAN}Pipe to 'dot -Tsvg > graph.svg' for visualization${NC}" >&2
|
|
fi
|
|
}
|
|
|
|
cmd_override_inputs() {
|
|
wants_help "$@" && show_help "Usage: ws override-inputs <repo...|--group name> [options]
|
|
|
|
Print the nix --override-input flags for building repos with local overrides.
|
|
|
|
Options:
|
|
--group, -g <name...> Use a named repo group (see 'ws groups')
|
|
--auto-local, -a Auto-detect dirty repos and use as local overrides
|
|
--local, -l <repo1> <repo2> Use specific repos as local overrides
|
|
|
|
Examples:
|
|
ws override-inputs logos-basecamp --auto-local
|
|
ws override-inputs --group core --auto-local
|
|
ws override-inputs logos-basecamp logos-liblogos --auto-local
|
|
ws override-inputs logos-basecamp --local logos-cpp-sdk"
|
|
|
|
local repos=()
|
|
local auto_local=false
|
|
local local_overrides=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--group|-g)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local _gexp; _gexp=$(expand_group "$1") || exit 1
|
|
while IFS= read -r r; do [[ -n "$r" ]] && repos+=("$r"); done <<< "$_gexp"
|
|
shift
|
|
done ;;
|
|
--auto-local|--autolocal|-a) auto_local=true; shift ;;
|
|
--local|-l)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local_overrides+=("$1"); shift
|
|
done ;;
|
|
-*) shift ;;
|
|
*) repos+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ${#repos[@]} -eq 0 ]]; then
|
|
echo -e "${RED}Error:${NC} no repo specified" >&2
|
|
echo "Usage: ws override-inputs <repo...> [options]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if $auto_local; then
|
|
for dir in $(get_dirty_repos); do
|
|
local name
|
|
name=$(get_input_name "$dir")
|
|
[[ -n "$name" ]] && local_overrides+=("$dir")
|
|
done
|
|
fi
|
|
|
|
if [[ ${#local_overrides[@]} -eq 0 ]]; then
|
|
echo -e "${YELLOW}No local overrides specified.${NC}"
|
|
echo "Use --auto-local or --local <repo1> <repo2> ..."
|
|
return
|
|
fi
|
|
|
|
local unique=($(printf '%s\n' "${local_overrides[@]}" | sort -u))
|
|
|
|
for repo in "${repos[@]}"; do
|
|
log "${BOLD}Override flags for building ${CYAN}$repo${NC}${BOLD}:${NC}"
|
|
log ""
|
|
echo -n "nix build .#$repo"
|
|
for dir in "${unique[@]}"; do
|
|
local input_name
|
|
input_name=$(get_input_name "$dir")
|
|
echo -n " \\"
|
|
echo ""
|
|
echo -n " --override-input $input_name path:./repos/$dir"
|
|
done
|
|
echo ""
|
|
|
|
if [[ ${#repos[@]} -gt 1 ]]; then
|
|
log ""
|
|
fi
|
|
done
|
|
}
|
|
|
|
cmd_loc() {
|
|
wants_help "$@" && show_help "Usage: ws loc [repo...|--group name] [options]
|
|
|
|
Count lines of code using tokei. Without a repo, counts the whole workspace.
|
|
|
|
Options:
|
|
--group, -g <name...> Use a named repo group (see 'ws groups')
|
|
--all Per-repo breakdown sorted by LOC
|
|
--no-nix, --code-only Exclude .nix, .lock, .json, .md files
|
|
|
|
Examples:
|
|
ws loc Count all code in workspace
|
|
ws loc logos-cpp-sdk Count code in a single repo
|
|
ws loc --group core Count code in all core repos
|
|
ws loc logos-cpp-sdk logos-module Count code in multiple repos
|
|
ws loc --all Per-repo breakdown
|
|
ws loc --all --no-nix Per-repo breakdown, code only"
|
|
|
|
local targets=()
|
|
local exclude_nix=false
|
|
local show_all=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--group|-g)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local _gexp; _gexp=$(expand_group "$1") || exit 1
|
|
while IFS= read -r r; do [[ -n "$r" ]] && targets+=("$r"); done <<< "$_gexp"
|
|
shift
|
|
done ;;
|
|
--no-nix|--code-only) exclude_nix=true; shift ;;
|
|
--all) show_all=true; shift ;;
|
|
-*) shift ;;
|
|
*) targets+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
if ! command -v tokei &>/dev/null; then
|
|
echo -e "${YELLOW}tokei not found, installing...${NC}" >&2
|
|
nix profile install --extra-experimental-features "nix-command flakes" nixpkgs#tokei 2>/dev/null || {
|
|
echo -e "${RED}Error:${NC} could not install tokei. Run 'ws develop' or install tokei manually." >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
local tokei_args=()
|
|
if $exclude_nix; then
|
|
tokei_args+=(-e '*.nix' -e '*.lock' -e 'flake.lock' -e '*.json' -e '*.md')
|
|
fi
|
|
|
|
if $show_all; then
|
|
# Per-repo breakdown
|
|
local total_header_shown=false
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name
|
|
dir_name=$(get_field "$entry" 2)
|
|
local repo_path="$REPOS_DIR/$dir_name"
|
|
[[ -d "$repo_path" ]] || continue
|
|
|
|
local count
|
|
count=$(tokei "$repo_path" ${tokei_args[@]+"${tokei_args[@]}"} --output json 2>/dev/null | \
|
|
sed -n 's/.*"Total":{[^}]*"code":\([0-9]*\).*/\1/p' 2>/dev/null || echo "0")
|
|
[[ -z "$count" ]] && count=0
|
|
|
|
if [[ "$count" -gt 0 ]]; then
|
|
printf " ${CYAN}%7s${NC} %s\n" "$count" "$dir_name"
|
|
fi
|
|
done | sort -rn -k1
|
|
|
|
echo ""
|
|
log "${BOLD}Total across all repos:${NC}"
|
|
tokei "$REPOS_DIR" ${tokei_args[@]+"${tokei_args[@]}"}
|
|
|
|
elif [[ ${#targets[@]} -gt 0 ]]; then
|
|
for target in "${targets[@]}"; do
|
|
local repo_path="$REPOS_DIR/$target"
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$target' not found" >&2
|
|
continue
|
|
fi
|
|
if [[ ${#targets[@]} -gt 1 ]]; then
|
|
log "${BOLD}── $target ──${NC}"
|
|
fi
|
|
tokei "$repo_path" ${tokei_args[@]+"${tokei_args[@]}"}
|
|
if [[ ${#targets[@]} -gt 1 ]]; then
|
|
log ""
|
|
fi
|
|
done
|
|
|
|
else
|
|
# Whole workspace
|
|
tokei "$REPOS_DIR" ${tokei_args[@]+"${tokei_args[@]}"}
|
|
fi
|
|
}
|
|
|
|
cmd_watch() {
|
|
wants_help "$@" && show_help "Usage: ws watch <test|build|run> [args]
|
|
|
|
Auto-run commands on file changes using watchexec.
|
|
|
|
Subcommands:
|
|
test <repo...|--group name> [opts] Re-run tests when repo files change
|
|
build <repo...|--group name> [opts] Re-build when repo files change
|
|
run <cmd> [-w repo ...] Run arbitrary command on file changes
|
|
|
|
Options (test/build):
|
|
--group, -g <name...> Use a named repo group (see 'ws groups')
|
|
|
|
Examples:
|
|
ws watch test logos-module
|
|
ws watch test --group core --auto-local
|
|
ws watch test logos-module logos-liblogos --auto-local
|
|
ws watch build logos-basecamp --auto-local
|
|
ws watch build --group core
|
|
ws watch build logos-basecamp logos-liblogos
|
|
ws watch run 'ws test logos-module' -w logos-module -w logos-cpp-sdk"
|
|
|
|
local subcmd="${1:-}"
|
|
shift || true
|
|
|
|
if ! command -v watchexec &>/dev/null; then
|
|
echo -e "${YELLOW}watchexec not found, installing...${NC}" >&2
|
|
nix profile install --extra-experimental-features "nix-command flakes" nixpkgs#watchexec 2>/dev/null || {
|
|
echo -e "${RED}Error:${NC} could not install watchexec. Run 'ws develop' or install watchexec manually." >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
case "$subcmd" in
|
|
test)
|
|
local repos=()
|
|
local extra_args=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--group|-g)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local _gexp; _gexp=$(expand_group "$1") || exit 1
|
|
while IFS= read -r r; do [[ -n "$r" ]] && repos+=("$r"); done <<< "$_gexp"
|
|
shift
|
|
done ;;
|
|
-*) extra_args+=("$1"); shift ;;
|
|
*) repos+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ${#repos[@]} -eq 0 ]]; then
|
|
echo -e "${RED}Usage:${NC} ws watch test <repo...|--group name> [ws-test-opts]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local watch_dirs=()
|
|
for repo in "${repos[@]}"; do
|
|
local repo_path="$REPOS_DIR/$repo"
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo' not found" >&2
|
|
exit 1
|
|
fi
|
|
watch_dirs+=(-w "$repo_path")
|
|
done
|
|
|
|
log "${BOLD}Watching ${CYAN}${repos[*]}${NC}${BOLD} for changes — running tests on save${NC}"
|
|
exec watchexec "${watch_dirs[@]}" --restart -- \
|
|
"$WORKSPACE_ROOT/scripts/ws" test "${repos[@]}" "${extra_args[@]}"
|
|
;;
|
|
|
|
build)
|
|
local repos=()
|
|
local extra_args=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--group|-g)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local _gexp; _gexp=$(expand_group "$1") || exit 1
|
|
while IFS= read -r r; do [[ -n "$r" ]] && repos+=("$r"); done <<< "$_gexp"
|
|
shift
|
|
done ;;
|
|
-*) extra_args+=("$1"); shift ;;
|
|
*) repos+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ${#repos[@]} -eq 0 ]]; then
|
|
echo -e "${RED}Usage:${NC} ws watch build <repo...|--group name> [ws-build-opts]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local watch_dirs=()
|
|
for repo in "${repos[@]}"; do
|
|
local repo_path="$REPOS_DIR/$repo"
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo' not found" >&2
|
|
exit 1
|
|
fi
|
|
watch_dirs+=(-w "$repo_path")
|
|
done
|
|
|
|
log "${BOLD}Watching ${CYAN}${repos[*]}${NC}${BOLD} for changes — building on save${NC}"
|
|
exec watchexec "${watch_dirs[@]}" --restart -- \
|
|
"$WORKSPACE_ROOT/scripts/ws" build "${repos[@]}" "${extra_args[@]}"
|
|
;;
|
|
|
|
run)
|
|
local cmd="${1:-}"
|
|
if [[ -z "$cmd" ]]; then
|
|
echo -e "${RED}Usage:${NC} ws watch run <command> [-w repo1 -w repo2 ...]" >&2
|
|
echo " Runs <command> whenever watched files change." >&2
|
|
echo "" >&2
|
|
echo "Examples:" >&2
|
|
echo " ws watch run 'ws test logos-module --auto-local' -w logos-module -w logos-cpp-sdk" >&2
|
|
echo " ws watch run 'echo changed' -w logos-liblogos" >&2
|
|
exit 1
|
|
fi
|
|
shift
|
|
|
|
# Collect -w flags and convert repo names to paths
|
|
local watch_args=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-w)
|
|
local repo_name="$2"
|
|
local rpath="$REPOS_DIR/$repo_name"
|
|
if [[ -d "$rpath" ]]; then
|
|
watch_args+=(-w "$rpath")
|
|
else
|
|
watch_args+=(-w "$repo_name")
|
|
fi
|
|
shift 2
|
|
;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ${#watch_args[@]} -eq 0 ]]; then
|
|
watch_args=(-w "$REPOS_DIR")
|
|
fi
|
|
|
|
log "${BOLD}Watching for changes...${NC}"
|
|
exec watchexec "${watch_args[@]}" --restart -- bash -c "$cmd"
|
|
;;
|
|
|
|
*)
|
|
echo -e "${RED}Usage:${NC} ws watch <test|build|run>" >&2
|
|
echo "" >&2
|
|
echo " test <repo...> [opts] Re-run tests on file changes" >&2
|
|
echo " build <repo...> [opts] Re-build on file changes" >&2
|
|
echo " run <cmd> [-w repo...] Run arbitrary command on file changes" >&2
|
|
echo "" >&2
|
|
echo "Examples:" >&2
|
|
echo " ws watch test logos-module" >&2
|
|
echo " ws watch test logos-module logos-liblogos --auto-local" >&2
|
|
echo " ws watch build logos-basecamp --auto-local" >&2
|
|
echo " ws watch run 'ws test logos-module --auto-local' -w logos-module -w logos-cpp-sdk" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cmd_foreach() {
|
|
wants_help "$@" && show_help "Usage: ws foreach <command>
|
|
|
|
Run a command in every cloned repo.
|
|
|
|
When the command is a single quoted string with spaces, it runs via bash -c
|
|
(so shell operators like && and || work).
|
|
|
|
Examples:
|
|
ws foreach git status -s
|
|
ws foreach 'git add . && git commit -m \"update\"'
|
|
ws foreach pwd"
|
|
|
|
local cmd=("$@")
|
|
|
|
if [[ ${#cmd[@]} -eq 0 ]]; then
|
|
echo -e "${RED}Usage:${NC} ws foreach <command>" >&2
|
|
exit 1
|
|
fi
|
|
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name
|
|
dir_name=$(get_field "$entry" 2)
|
|
local repo_path="$REPOS_DIR/$dir_name"
|
|
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
continue
|
|
fi
|
|
|
|
log "${BOLD}── $dir_name ──${NC}"
|
|
(cd "$repo_path" && if [[ ${#cmd[@]} -eq 1 && "${cmd[0]}" == *" "* ]]; then bash -c "${cmd[0]}"; else "${cmd[@]}"; fi) || true
|
|
log ""
|
|
done
|
|
}
|
|
|
|
cmd_worktree() {
|
|
wants_help "$@" && show_help "Usage: ws worktree <add|list|remove>
|
|
|
|
Manage workspace worktrees for isolated multi-repo feature branches.
|
|
|
|
Subcommands:
|
|
add <name> [-b branch] Create a worktree with submodules and ws/<branch> branches
|
|
list List all worktrees
|
|
remove <name> Remove a worktree
|
|
|
|
Examples:
|
|
ws worktree add my-feature
|
|
ws worktree add my-feature -b custom-branch
|
|
ws worktree list
|
|
ws worktree remove my-feature"
|
|
|
|
local subcmd="${1:-}"
|
|
shift || true
|
|
|
|
case "$subcmd" in
|
|
add)
|
|
local name=""
|
|
local branch=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-b) branch="$2"; shift 2 ;;
|
|
*) name="$1"; shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$name" ]]; then
|
|
echo -e "${RED}Usage:${NC} ws worktree add <name> [-b branch]" >&2
|
|
echo " Creates a workspace worktree with submodules initialized." >&2
|
|
echo " Each subrepo gets a ws/<branch> branch." >&2
|
|
exit 1
|
|
fi
|
|
|
|
branch="${branch:-$name}"
|
|
local wt_path="$WORKSPACE_ROOT/../logos-workspace--$name"
|
|
|
|
if [[ -d "$wt_path" ]]; then
|
|
echo -e "${RED}Worktree already exists:${NC} $wt_path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
log "${BOLD}Creating workspace worktree...${NC}"
|
|
git -C "$WORKSPACE_ROOT" worktree add "$wt_path" -b "$branch"
|
|
|
|
log "${BOLD}Initializing submodules...${NC}"
|
|
(cd "$wt_path" && git submodule update --init --depth 1 --jobs 4)
|
|
|
|
local repo_branch="ws/$branch"
|
|
log "${BOLD}Creating branch ${CYAN}$repo_branch${NC}${BOLD} in all repos...${NC}"
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name
|
|
dir_name=$(get_field "$entry" 2)
|
|
local repo_path="$wt_path/repos/$dir_name"
|
|
if [[ -d "$repo_path/.git" ]] || [[ -f "$repo_path/.git" ]]; then
|
|
(cd "$repo_path" && git checkout -b "$repo_branch" 2>/dev/null && \
|
|
log " ${GREEN}✓${NC} $dir_name") || \
|
|
log " ${YELLOW}skip${NC} $dir_name (branch may already exist)"
|
|
fi
|
|
done
|
|
|
|
log ""
|
|
echo -e "${GREEN}Worktree ready:${NC} $wt_path"
|
|
echo -e " Workspace branch: ${CYAN}$branch${NC}"
|
|
echo -e " Subrepo branches: ${CYAN}$repo_branch${NC}"
|
|
;;
|
|
|
|
list)
|
|
git -C "$WORKSPACE_ROOT" worktree list
|
|
;;
|
|
|
|
remove)
|
|
local name="${1:-}"
|
|
if [[ -z "$name" ]]; then
|
|
echo -e "${RED}Usage:${NC} ws worktree remove <name>" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local wt_path="$WORKSPACE_ROOT/../logos-workspace--$name"
|
|
|
|
if [[ ! -d "$wt_path" ]]; then
|
|
echo -e "${RED}Worktree not found:${NC} $wt_path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
log "${BOLD}Removing worktree...${NC}"
|
|
git -C "$WORKSPACE_ROOT" worktree remove "$wt_path" --force
|
|
echo -e "${GREEN}Removed:${NC} $wt_path"
|
|
;;
|
|
|
|
*)
|
|
echo -e "${RED}Usage:${NC} ws worktree <add|list|remove>" >&2
|
|
echo " add <name> [-b branch] Create a worktree with submodules and branches" >&2
|
|
echo " list List all worktrees" >&2
|
|
echo " remove <name> Remove a worktree" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cmd_update() {
|
|
wants_help "$@" && show_help "Usage: ws update [repo...|--group name] [--deep]
|
|
|
|
Update flake.lock inputs. Without a repo, updates all inputs.
|
|
|
|
Options:
|
|
--group, -g <name...> Use a named repo group (see 'ws groups')
|
|
--deep Also update in every sub-repo that depends on the target
|
|
|
|
Without --deep, only the workspace flake.lock is updated.
|
|
With --deep, every repo that lists the target as a direct flake input
|
|
gets its own flake.lock updated too. This ensures consistent versions
|
|
across the entire dependency tree.
|
|
|
|
Examples:
|
|
ws update Update all flake inputs
|
|
ws update logos-cpp-sdk Update a single input in workspace flake.lock
|
|
ws update --deep logos-cpp-sdk Update logos-cpp-sdk everywhere (workspace + all sub-repos)
|
|
ws update --group core Update all core group inputs
|
|
ws update logos-cpp-sdk logos-module Update multiple inputs"
|
|
|
|
local targets=()
|
|
local deep=false
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--group|-g)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local _gexp; _gexp=$(expand_group "$1") || exit 1
|
|
while IFS= read -r r; do [[ -n "$r" ]] && targets+=("$r"); done <<< "$_gexp"
|
|
shift
|
|
done ;;
|
|
--all) targets=(); break ;;
|
|
--deep) deep=true; shift ;;
|
|
-*) shift ;;
|
|
*) targets+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ${#targets[@]} -eq 0 ]]; then
|
|
log "${BOLD}Updating all flake inputs...${NC}"
|
|
nix flake update \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT"
|
|
else
|
|
for target in "${targets[@]}"; do
|
|
local input_name
|
|
input_name=$(get_input_name "$target")
|
|
log "${BOLD}Updating ${CYAN}$input_name${NC}${BOLD} in workspace...${NC}"
|
|
nix flake lock \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
--update-input "$input_name" \
|
|
"$WORKSPACE_ROOT"
|
|
|
|
if $deep; then
|
|
# Find all repos that have this target as a direct dependency
|
|
local dependents
|
|
dependents=$(get_dependents "$input_name")
|
|
if [[ -n "$dependents" ]]; then
|
|
while IFS= read -r dep_input; do
|
|
[[ -z "$dep_input" ]] && continue
|
|
# Map input name back to directory name
|
|
local dep_entry dep_dir
|
|
dep_entry=$(find_repo_entry "$dep_input") || continue
|
|
dep_dir=$(get_field "$dep_entry" 2)
|
|
local dep_path="$REPOS_DIR/$dep_dir"
|
|
|
|
if [[ ! -d "$dep_path" ]] || [[ ! -f "$dep_path/flake.nix" ]]; then
|
|
continue
|
|
fi
|
|
|
|
log " ${YELLOW}*${NC} Updating ${CYAN}$input_name${NC} in ${BOLD}$dep_dir${NC}/flake.lock"
|
|
nix flake lock \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
--update-input "$input_name" \
|
|
"path:$dep_path" || {
|
|
echo -e " ${RED}WARN${NC} Failed to update $input_name in $dep_dir"
|
|
}
|
|
done <<< "$dependents"
|
|
else
|
|
log " No sub-repos depend on ${CYAN}$input_name${NC}"
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
}
|
|
|
|
cmd_list() {
|
|
wants_help "$@" && show_help "Usage: ws list
|
|
|
|
List all repos in the workspace with their flake and clone status.
|
|
|
|
Examples:
|
|
ws list"
|
|
|
|
log "${BOLD}Workspace repos (${#REPOS[@]} total)${NC}"
|
|
log ""
|
|
|
|
local current_section=""
|
|
for entry in "${REPOS[@]}"; do
|
|
local input_name dir_name has_flake
|
|
input_name=$(get_field "$entry" 1)
|
|
dir_name=$(get_field "$entry" 2)
|
|
has_flake=$(get_field "$entry" 4)
|
|
|
|
local flake_marker=""
|
|
if [[ "$has_flake" == "yes" ]]; then
|
|
flake_marker="${GREEN}flake${NC}"
|
|
else
|
|
flake_marker="${YELLOW}no-flake${NC}"
|
|
fi
|
|
|
|
local exists_marker=""
|
|
if [[ -d "$REPOS_DIR/$dir_name/.git" ]] || [[ -f "$REPOS_DIR/$dir_name/.git" ]]; then
|
|
exists_marker="${GREEN}cloned${NC}"
|
|
else
|
|
exists_marker="${RED}not-cloned${NC}"
|
|
fi
|
|
|
|
printf " %-35s %-15b %b\n" "$dir_name" "$flake_marker" "$exists_marker"
|
|
done
|
|
}
|
|
|
|
# ── URL overrides for repos not at github:logos-co/<name> ──────────────────
|
|
# Used by `ws sync-graph` when generating flake.nix's auto-inputs section.
|
|
# Base URL (org/repo) — override for repos outside logos-co or with ?dir=.
|
|
get_base_repo_url() {
|
|
case "$1" in
|
|
logos-blockchain-module) echo "github:logos-blockchain/logos-blockchain-module" ;;
|
|
logos-blockchain-ui) echo "github:logos-blockchain/logos-blockchain-ui" ;;
|
|
logos-execution-zone-module) echo "github:logos-blockchain/logos-execution-zone-module" ;;
|
|
logos-execution-zone-wallet-ui) echo "github:logos-blockchain/logos-execution-zone-wallet-ui" ;;
|
|
*) echo "github:logos-co/$1" ;;
|
|
esac
|
|
}
|
|
|
|
# Returns the flake URL for a repo, always pinning to the workspace submodule
|
|
# commit so flake.nix tracks the exact same revision as the submodule pointer.
|
|
get_repo_url() {
|
|
local input_name="$1"
|
|
local base_url
|
|
base_url=$(get_base_repo_url "$input_name")
|
|
|
|
# Find the repo's directory from the REPOS table
|
|
local entry dir_name repo_dir
|
|
entry=$(find_repo_entry "$input_name") || { echo "$base_url"; return; }
|
|
dir_name=$(get_field "$entry" 2)
|
|
repo_dir="$REPOS_DIR/$dir_name"
|
|
|
|
[[ -d "$repo_dir" ]] || { echo "$base_url"; return; }
|
|
|
|
# Get the submodule commit that the workspace has checked out.
|
|
# Read from the submodule's own HEAD (not the superproject's committed
|
|
# gitlink) so we pick up bumps that haven't been committed yet — e.g. the
|
|
# nightly workflow runs `ws sync-graph` between `git submodule update
|
|
# --remote` and the eventual commit.
|
|
local sub_rev
|
|
sub_rev=$(git -C "$repo_dir" rev-parse HEAD 2>/dev/null)
|
|
[[ -n "$sub_rev" ]] || { echo "$base_url"; return; }
|
|
|
|
# Always pin to the submodule rev — append /rev for simple URLs,
|
|
# or &rev=... for URLs with ?dir= query params.
|
|
case "$base_url" in
|
|
*\?*) echo "${base_url}&rev=${sub_rev}" ;;
|
|
*) echo "${base_url}/${sub_rev}" ;;
|
|
esac
|
|
}
|
|
|
|
# ── Repos that must build from their OWN locked inputs even under
|
|
# --workspace, i.e. sync-graph emits NO workspace `follows` for them.
|
|
# Each entry is a deliberate divergence and must say why.
|
|
follows_exempt() {
|
|
case "$1" in
|
|
# logos-rust-sdk's C-FFI codegen needs logos-cpp-sdk's `c_ffi` branch:
|
|
# the `support c-ffi` commit (d98e2a9) that emits the provider
|
|
# inter-module API header was never merged to cpp-sdk master. Following
|
|
# the workspace (master) cpp-sdk breaks the caller module's generated
|
|
# code (`fatal error: sdk_test_provider_module_api.h: No such file`), so
|
|
# keep it on its own c_ffi pins. Revisit once c-ffi lands on cpp-sdk master.
|
|
logos-rust-sdk) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# ── Workspace-level follows that override what each repo's lock would
|
|
# otherwise produce. Every entry is a deliberate divergence from upstream
|
|
# and should have a comment explaining why. Returns tab-separated
|
|
# `input_name<TAB>target` lines per repo.
|
|
get_manual_follows() {
|
|
case "$1" in
|
|
# These three repos declare an input named `logos-package-manager` that
|
|
# actually points at `logos-package-manager-module` upstream. The repos
|
|
# consume `<input>.packages.${system}.cli`, which only exists on the
|
|
# real `logos-package-manager` library — so the workspace retargets it.
|
|
logos-accounts-ui|logos-execution-zone-wallet-ui|logos-module-viewer)
|
|
printf 'logos-package-manager\tlogos-package-manager\n' ;;
|
|
# logos-blockchain-module's upstream lock currently resolves
|
|
# logos-blockchain as github:logos-blockchain/logos-blockchain/0.2.1
|
|
# which Nix interprets as a commit-ish and fails to fetch.
|
|
# Force it to follow the workspace-level pinned input instead.
|
|
logos-blockchain-module)
|
|
printf 'logos-blockchain\tlogos-blockchain\n' ;;
|
|
# keystore + token-list ship no committed flake.lock, so sync-graph can't
|
|
# auto-derive their module-builder dep — declare it manually so the
|
|
# workspace overrides it (version consistency with the rest of the tree).
|
|
logos-evm-keystore-module|logos-evm-token-list-module)
|
|
printf 'logos-module-builder\tlogos-module-builder\n' ;;
|
|
esac
|
|
}
|
|
|
|
# ── Workspace-level URL overrides for a downstream repo's TRANSITIVE inputs.
|
|
# Unlike follows (which retarget a transitive input to a workspace input),
|
|
# these pin a transitive input to an explicit flake ref. Use when upstream
|
|
# pins a ref that has become unresolvable (e.g. a deleted branch), which would
|
|
# otherwise make `nix flake lock --update-input <repo>` (the nightly bump) fail
|
|
# to resolve that ref. Returns tab-separated `input_name<TAB>flake_ref` lines
|
|
# per repo. Emitted into the flake.nix input block after the follows lines.
|
|
get_manual_url_overrides() {
|
|
case "$1" in
|
|
# logos-libp2p-module pins its `libp2p` input to the upstream
|
|
# `feat/cbind/xpr-decode` branch, which was deleted — so re-locking it
|
|
# 422s on branch resolution. Pin to the exact SHA that branch last pointed
|
|
# at (never merged to master) until the module pins the SHA itself.
|
|
logos-libp2p-module)
|
|
printf 'libp2p\tgithub:vacp2p/nim-libp2p/7c73484cc0c57a5f649dd3d277cef1c7c4de28f0\n' ;;
|
|
esac
|
|
}
|
|
|
|
# Parse a flake.nix and emit its declared root input names, one per line.
|
|
# Handles both styles:
|
|
# inputs.foo.url = "...";
|
|
# inputs = { foo.url = "..."; bar = { ... }; };
|
|
get_declared_input_names() {
|
|
local flake_file="$1"
|
|
awk '
|
|
BEGIN { in_inputs = 0; depth = 0 }
|
|
{
|
|
line = $0
|
|
|
|
if (!in_inputs) {
|
|
# Top-level attrpath form: `inputs.<name>.<...> = ...;`
|
|
if (match(line, /^[[:space:]]*inputs\.[^[:space:].={]+/)) {
|
|
key = substr(line, RSTART, RLENGTH)
|
|
sub(/^[[:space:]]*inputs\./, "", key)
|
|
if (length(key) > 0) print key
|
|
}
|
|
# Attrset block form: `inputs = { ... };`
|
|
if (match(line, /^[[:space:]]*inputs[[:space:]]*=[[:space:]]*\{/)) {
|
|
in_inputs = 1
|
|
depth = 1
|
|
}
|
|
next
|
|
}
|
|
|
|
# Capture names assigned at the top level of inputs attrset.
|
|
if (depth == 1 && match(line, /^[[:space:]]*[^[:space:]=]+[[:space:]]*=/)) {
|
|
key = substr(line, RSTART, RLENGTH)
|
|
sub(/^[[:space:]]*/, "", key)
|
|
sub(/[[:space:]]*=.*/, "", key)
|
|
sub(/\..*$/, "", key)
|
|
gsub(/^"/, "", key)
|
|
gsub(/"$/, "", key)
|
|
if (length(key) > 0) print key
|
|
}
|
|
|
|
opens = gsub(/\{/, "{", line)
|
|
closes = gsub(/\}/, "}", line)
|
|
depth += opens - closes
|
|
if (depth <= 0) {
|
|
in_inputs = 0
|
|
depth = 0
|
|
}
|
|
}
|
|
' "$flake_file" | sort -u
|
|
}
|
|
|
|
cmd_sync_graph() {
|
|
wants_help "$@" && show_help "Usage: ws sync-graph
|
|
|
|
Regenerate nix/dep-graph.nix and the auto-inputs section of flake.nix by
|
|
scanning each repo's flake.lock for workspace dependencies and follows
|
|
mappings, plus its flake.nix for test outputs.
|
|
|
|
Nix's flake parser requires \`inputs\` to be a literal attrset (no \`let\`,
|
|
no \`//\`, no \`import\`), so the per-repo inputs in flake.nix must be
|
|
generated by this command rather than computed at evaluation time.
|
|
|
|
Examples:
|
|
ws sync-graph"
|
|
|
|
log "${BOLD}Syncing nix/dep-graph.nix and flake.nix inputs from repo flake.lock files...${NC}"
|
|
log ""
|
|
|
|
# Build list of workspace input names (in REPOS order), as a space-padded
|
|
# string for fast membership testing in bash 3.2 (no associative arrays).
|
|
# `ws_names_ordered` preserves REPOS order; `ws_names_set` is padded with
|
|
# leading/trailing spaces so `case "$ws_names_set" in *" $name "*)` matches.
|
|
# `ws_names_set` also includes synthetic workspace-level inputs that have
|
|
# no submodule (nixpkgs) so that downstream follows for them are emitted
|
|
# into dep-graph.nix. These are excluded from `deps` (they aren't repos).
|
|
local ws_names_ordered=""
|
|
for entry in "${REPOS[@]}"; do
|
|
local iname
|
|
iname=$(get_field "$entry" 1)
|
|
[[ -n "$iname" ]] && ws_names_ordered+="$iname "
|
|
done
|
|
local ws_names_set=" $ws_names_ordered nixpkgs "
|
|
|
|
# Find max name length for alignment
|
|
local max_len=0
|
|
for name in $ws_names_ordered; do
|
|
[[ ${#name} -gt $max_len ]] && max_len=${#name}
|
|
done
|
|
|
|
local tmpfile flake_inputs_tmp
|
|
tmpfile=$(mktemp)
|
|
flake_inputs_tmp=$(mktemp)
|
|
|
|
cat > "$tmpfile" << 'HEADER'
|
|
# Dependency graph: flake input name → { deps, follows, hasTests }
|
|
#
|
|
# follows: map from this repo's downstream-input-name → workspace input name
|
|
# it should follow. Auto-derived from each repo's flake.lock by
|
|
# parsing root inputs (string refs become URL→workspace lookups,
|
|
# array refs are emitted when the input name itself matches a
|
|
# workspace input).
|
|
# deps: unique workspace input names this repo depends on (= unique values
|
|
# of follows, in REPOS table order, with "nixpkgs" elided). Used by
|
|
# `ws dirty`, `ws graph`, and CI to compute downstream dependents.
|
|
# hasTests: whether this repo exposes checks or test packages in its flake.
|
|
# Used by `ws test` to skip expensive nix eval for repos without tests.
|
|
#
|
|
# AUTO-GENERATED by `ws sync-graph`. To update: ws sync-graph
|
|
#
|
|
# Consumed by:
|
|
# - scripts/ws: ws dirty / ws graph / has_tests / get_dependents
|
|
# - .github/workflows/ci.yml: change-detection grep for `"$repo"` deps lines
|
|
# - flake.nix: `lib.depGraph` re-export for programmatic introspection
|
|
{
|
|
HEADER
|
|
|
|
for entry in "${REPOS[@]}"; do
|
|
local input_name dir_name has_flake
|
|
input_name=$(get_field "$entry" 1)
|
|
dir_name=$(get_field "$entry" 2)
|
|
has_flake=$(get_field "$entry" 4)
|
|
|
|
[[ "$has_flake" != "yes" || -z "$input_name" ]] && continue
|
|
|
|
local repo_dir="$REPOS_DIR/$dir_name"
|
|
local flake_file="$repo_dir/flake.nix"
|
|
local lock_file="$repo_dir/flake.lock"
|
|
|
|
if [[ ! -f "$flake_file" ]]; then
|
|
log " ${YELLOW}skip${NC} $dir_name (not cloned)"
|
|
continue
|
|
fi
|
|
|
|
# Only emit follows for inputs currently declared in flake.nix.
|
|
# flake.lock can retain stale root-input keys after input removal.
|
|
local declared_inputs
|
|
declared_inputs="$(get_declared_input_names "$flake_file")"
|
|
local declared_set=" $(printf '%s\n' "$declared_inputs" | tr '\n' ' ') "
|
|
|
|
# ── Parse flake.lock → tab-separated `iname<TAB>ws_id` per accepted input
|
|
# `follows_pairs` is a single multi-line string (bash 3.2 has no
|
|
# associative arrays). Lines are sorted alphabetically by iname later.
|
|
local follows_pairs=""
|
|
if ! follows_exempt "$input_name" && [[ -f "$lock_file" ]]; then
|
|
# jq emits one tab-separated record per root input:
|
|
# <kind>\t<input_name>\t<repo>\t<dir>
|
|
# where <kind> is "url" (string ref to a node) or "follows" (array ref,
|
|
# i.e. a pure follows declaration in the downstream flake).
|
|
local jq_out
|
|
jq_out=$(jq -r '
|
|
. as $r
|
|
| $r.nodes[$r.root].inputs
|
|
| to_entries[]
|
|
| .key as $iname
|
|
| .value as $v
|
|
| if ($v | type) == "string" then
|
|
($r.nodes[$v].original // {}) as $o
|
|
| "url\t\($iname)\t\($o.repo // "")\t\($o.dir // "")"
|
|
else
|
|
"follows\t\($iname)\t\t"
|
|
end
|
|
' "$lock_file" 2>/dev/null) || jq_out=""
|
|
|
|
while IFS=$'\t' read -r kind iname repo dir; do
|
|
[[ -z "$iname" ]] && continue
|
|
case "$declared_set" in
|
|
*" $iname "*) ;;
|
|
*) continue ;;
|
|
esac
|
|
|
|
local ws_id=""
|
|
if [[ "$kind" == "url" ]]; then
|
|
# Tutorial sub-flakes encode the actual sub-repo as `?dir=...`
|
|
# (e.g., github:logos-co/logos-tutorial?dir=logos-calc-module).
|
|
# Use the dir basename when present, otherwise the repo name.
|
|
if [[ -n "$dir" ]]; then
|
|
ws_id="${dir##*/}"
|
|
else
|
|
ws_id="$repo"
|
|
fi
|
|
elif [[ "$kind" == "follows" ]]; then
|
|
# Pure follows declaration in the downstream flake — only override
|
|
# at workspace level if the input name itself matches a workspace
|
|
# input (the conventional `nixpkgs` / `logos-nix` follows pattern).
|
|
ws_id="$iname"
|
|
fi
|
|
|
|
# Skip self-references and non-workspace targets
|
|
[[ -z "$ws_id" || "$ws_id" == "$input_name" ]] && continue
|
|
case "$ws_names_set" in
|
|
*" $ws_id "*) follows_pairs+="${iname} ${ws_id}
|
|
" ;;
|
|
esac
|
|
done <<< "$jq_out"
|
|
fi
|
|
|
|
# ── Apply manual workspace-level follows overrides ───────────────────
|
|
# Manual entries override (or add to) the auto-derived follows. We
|
|
# rebuild `follows_pairs` keeping only the latest entry per iname.
|
|
local manual_follows
|
|
manual_follows=$(get_manual_follows "$input_name")
|
|
if [[ -n "$manual_follows" ]]; then
|
|
local merged=""
|
|
# First, drop auto entries whose iname appears in manual_follows
|
|
while IFS=$'\t' read -r f_iname f_target; do
|
|
[[ -z "$f_iname" ]] && continue
|
|
local override_target=""
|
|
while IFS=$'\t' read -r m_iname m_target; do
|
|
[[ -z "$m_iname" ]] && continue
|
|
[[ "$m_iname" == "$f_iname" ]] && override_target="$m_target"
|
|
done <<< "$manual_follows"
|
|
if [[ -n "$override_target" ]]; then
|
|
merged+="${f_iname} ${override_target}
|
|
"
|
|
else
|
|
merged+="${f_iname} ${f_target}
|
|
"
|
|
fi
|
|
done <<< "$follows_pairs"
|
|
# Then append any manual entries not already present in follows_pairs
|
|
while IFS=$'\t' read -r m_iname m_target; do
|
|
[[ -z "$m_iname" ]] && continue
|
|
case "$declared_set" in
|
|
*" $m_iname "*) ;;
|
|
*) continue ;;
|
|
esac
|
|
case "$follows_pairs" in
|
|
*"$m_iname "*) ;;
|
|
*) merged+="${m_iname} ${m_target}
|
|
" ;;
|
|
esac
|
|
done <<< "$manual_follows"
|
|
follows_pairs="$merged"
|
|
fi
|
|
|
|
# Sort follows pairs alphabetically by input name for deterministic output
|
|
local follows_sorted=""
|
|
if [[ -n "$follows_pairs" ]]; then
|
|
follows_sorted=$(printf '%s' "$follows_pairs" | sort)
|
|
fi
|
|
|
|
# ── Compute deps: unique workspace targets in REPOS order ────────────
|
|
# Skip `nixpkgs` (not a tracked repo) and self-references. Each ws name
|
|
# is checked against the second column of follows_pairs.
|
|
local unique_targets=""
|
|
if [[ -n "$follows_sorted" ]]; then
|
|
unique_targets=" $(printf '%s\n' "$follows_sorted" | awk -F'\t' '{print $2}' | sort -u | tr '\n' ' ') "
|
|
fi
|
|
local deps_str="[]"
|
|
local deps_acc=""
|
|
for ws in $ws_names_ordered; do
|
|
[[ "$ws" == "$input_name" ]] && continue
|
|
case "$unique_targets" in
|
|
*" $ws "*) deps_acc+="\"$ws\" " ;;
|
|
esac
|
|
done
|
|
[[ -n "$deps_acc" ]] && deps_str="[ $deps_acc]"
|
|
|
|
# ── Format follows attrset (alphabetically sorted by iname) ──────────
|
|
local follows_str="{}"
|
|
if [[ -n "$follows_sorted" ]]; then
|
|
follows_str="{"
|
|
while IFS=$'\t' read -r f_iname f_target; do
|
|
[[ -z "$f_iname" ]] && continue
|
|
follows_str+=" \"${f_iname}\" = \"${f_target}\";"
|
|
done <<< "$follows_sorted"
|
|
follows_str+=" }"
|
|
fi
|
|
|
|
# ── Check hasTests: does the flake declare a checks output? ──────────
|
|
local has_tests="false"
|
|
if grep -qE '^[[:space:]]+checks[[:space:]]*=' "$flake_file" 2>/dev/null; then
|
|
has_tests="true"
|
|
fi
|
|
|
|
# ── Write dep-graph entry with name padding ──────────────────────────
|
|
local pad
|
|
pad=$(printf "%-${max_len}s" "$input_name")
|
|
echo " ${pad} = { deps = ${deps_str}; follows = ${follows_str}; hasTests = ${has_tests}; };" >> "$tmpfile"
|
|
|
|
# ── Write flake.nix input entry ──────────────────────────────────────
|
|
# Skip logos-nix here — it's declared in the manual section of flake.nix
|
|
# because it's the root nixpkgs pin and `nixpkgs.follows` references it.
|
|
if [[ "$input_name" != "logos-nix" ]]; then
|
|
local repo_url
|
|
repo_url=$(get_repo_url "$input_name")
|
|
|
|
# ── Resolve transitive URL overrides (see get_manual_url_overrides) ──
|
|
# Keep only inames the downstream flake actually declares (same guard as
|
|
# the follows path) so a renamed/removed upstream input can't leave a
|
|
# stale override that adds a spurious lock node. Sort for deterministic,
|
|
# idempotent output when a repo has more than one override.
|
|
local url_overrides url_overrides_sorted=""
|
|
url_overrides=$(get_manual_url_overrides "$input_name")
|
|
if [[ -n "$url_overrides" ]]; then
|
|
local url_kept=""
|
|
while IFS=$'\t' read -r u_iname u_url; do
|
|
[[ -z "$u_iname" ]] && continue
|
|
case "$declared_set" in
|
|
*" $u_iname "*) url_kept+="${u_iname} ${u_url}
|
|
" ;;
|
|
*) log " ${YELLOW}skip${NC} url-override ${input_name}/${u_iname} (not a declared input)" ;;
|
|
esac
|
|
done <<< "$url_overrides"
|
|
[[ -n "$url_kept" ]] && url_overrides_sorted=$(printf '%s' "$url_kept" | sort)
|
|
fi
|
|
|
|
if [[ -z "$follows_sorted" && -z "$url_overrides_sorted" ]]; then
|
|
echo " ${input_name}.url = \"${repo_url}\";" >> "$flake_inputs_tmp"
|
|
else
|
|
echo " ${input_name} = {" >> "$flake_inputs_tmp"
|
|
echo " url = \"${repo_url}\";" >> "$flake_inputs_tmp"
|
|
while IFS=$'\t' read -r f_iname f_target; do
|
|
[[ -z "$f_iname" ]] && continue
|
|
echo " inputs.${f_iname}.follows = \"${f_target}\";" >> "$flake_inputs_tmp"
|
|
done <<< "$follows_sorted"
|
|
# URL overrides go after the follows so a deleted upstream ref can't
|
|
# break the workspace re-lock.
|
|
if [[ -n "$url_overrides_sorted" ]]; then
|
|
while IFS=$'\t' read -r u_iname u_url; do
|
|
[[ -z "$u_iname" ]] && continue
|
|
echo " inputs.${u_iname}.url = \"${u_url}\";" >> "$flake_inputs_tmp"
|
|
done <<< "$url_overrides_sorted"
|
|
fi
|
|
echo " };" >> "$flake_inputs_tmp"
|
|
fi
|
|
fi
|
|
|
|
# Console status
|
|
local follow_count
|
|
follow_count=$(printf '%s' "$follows_pairs" | grep -c ' ' || true)
|
|
local marker="${GREEN}ok${NC}"
|
|
[[ "$has_tests" == "true" ]] && marker="${GREEN}ok${NC} ${CYAN}(tests)${NC}"
|
|
log " $marker $input_name (${follow_count} follows)"
|
|
done
|
|
|
|
echo "}" >> "$tmpfile"
|
|
log ""
|
|
|
|
# ── Update nix/dep-graph.nix ────────────────────────────────────────────
|
|
local dep_graph_changed=false
|
|
if [[ -f "$DEP_GRAPH" ]] && diff -q "$DEP_GRAPH" "$tmpfile" >/dev/null 2>&1; then
|
|
echo -e "${GREEN}No changes${NC} — nix/dep-graph.nix is already up to date."
|
|
rm "$tmpfile"
|
|
else
|
|
if [[ -f "$DEP_GRAPH" ]]; then
|
|
echo -e "${BOLD}Changes to nix/dep-graph.nix:${NC}"
|
|
diff --color=auto -u "$DEP_GRAPH" "$tmpfile" || true
|
|
echo ""
|
|
fi
|
|
mv "$tmpfile" "$DEP_GRAPH"
|
|
echo -e "${GREEN}Updated${NC} nix/dep-graph.nix"
|
|
dep_graph_changed=true
|
|
fi
|
|
|
|
# ── Update auto-inputs section in flake.nix ─────────────────────────────
|
|
local flake_file="$WORKSPACE_ROOT/flake.nix"
|
|
|
|
if [[ ! -f "$flake_file" ]]; then
|
|
echo -e "${RED}Error${NC}: $flake_file not found" >&2
|
|
rm -f "$flake_inputs_tmp"
|
|
return 1
|
|
fi
|
|
|
|
local has_begin has_end
|
|
has_begin=$(grep -cF "# BEGIN AUTO-INPUTS" "$flake_file")
|
|
has_end=$(grep -cF "# END AUTO-INPUTS" "$flake_file")
|
|
if [[ "$has_begin" -ne 1 || "$has_end" -ne 1 ]]; then
|
|
echo -e "${RED}Error${NC}: flake.nix must contain exactly one '# BEGIN AUTO-INPUTS' and one '# END AUTO-INPUTS' marker line (found BEGIN=$has_begin END=$has_end)" >&2
|
|
echo " Add the markers inside the \`inputs = { ... };\` block, e.g.:" >&2
|
|
echo " # BEGIN AUTO-INPUTS — generated by \`ws sync-graph\`, do not edit by hand" >&2
|
|
echo " # END AUTO-INPUTS" >&2
|
|
rm -f "$flake_inputs_tmp"
|
|
return 1
|
|
fi
|
|
|
|
local new_flake_tmp
|
|
new_flake_tmp=$(mktemp)
|
|
awk -v begin="# BEGIN AUTO-INPUTS" -v endm="# END AUTO-INPUTS" -v body_file="$flake_inputs_tmp" '
|
|
BEGIN { skipping = 0 }
|
|
{
|
|
if (index($0, begin) > 0) {
|
|
print
|
|
while ((getline line < body_file) > 0) print line
|
|
close(body_file)
|
|
skipping = 1
|
|
next
|
|
}
|
|
if (skipping && index($0, endm) > 0) {
|
|
skipping = 0
|
|
print
|
|
next
|
|
}
|
|
if (!skipping) print
|
|
}
|
|
' "$flake_file" > "$new_flake_tmp"
|
|
rm -f "$flake_inputs_tmp"
|
|
|
|
if diff -q "$flake_file" "$new_flake_tmp" >/dev/null 2>&1; then
|
|
echo -e "${GREEN}No changes${NC} — flake.nix auto-inputs section is already up to date."
|
|
rm "$new_flake_tmp"
|
|
else
|
|
echo -e "${BOLD}Changes to flake.nix:${NC}"
|
|
diff --color=auto -u "$flake_file" "$new_flake_tmp" | head -200 || true
|
|
echo ""
|
|
mv "$new_flake_tmp" "$flake_file"
|
|
echo -e "${GREEN}Updated${NC} flake.nix auto-inputs section"
|
|
fi
|
|
}
|
|
|
|
cmd_update_order() {
|
|
wants_help "$@" && show_help "Usage: ws update-order <dep> [--for <target>] [--json] [--commands]
|
|
|
|
Show the topological order for propagating a dependency change.
|
|
|
|
When a dependency (e.g. logos-module) gets a new commit merged, all repos
|
|
that depend on it need their flake.lock updated — but in the right order.
|
|
Repos at the same level can be updated in parallel; each level must be
|
|
committed and pushed to GitHub before the next level can run nix flake lock
|
|
(since nix fetches from remote URLs, not local paths).
|
|
|
|
Options:
|
|
--for <target> Scope output to repos on paths leading to this target
|
|
--json Output machine-readable JSON
|
|
--commands Also print nix flake lock + git commands per level
|
|
|
|
Examples:
|
|
ws update-order logos-module
|
|
ws update-order logos-cpp-sdk --for logos-basecamp
|
|
ws update-order logos-module-builder --commands
|
|
ws update-order logos-module --json"
|
|
|
|
local dep=""
|
|
local for_target=""
|
|
local json=false
|
|
local commands=false
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--for) for_target="$2"; shift 2 ;;
|
|
--json) json=true; shift ;;
|
|
--commands) commands=true; shift ;;
|
|
-*) shift ;;
|
|
*) dep="$1"; shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$dep" ]]; then
|
|
echo -e "${RED}Error:${NC} dependency name required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v jq &>/dev/null; then
|
|
echo -e "${RED}jq is required but not found${NC}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local input_name
|
|
input_name=$(get_input_name "$dep")
|
|
local for_input=""
|
|
[[ -n "$for_target" ]] && for_input=$(get_input_name "$for_target")
|
|
|
|
local levels_raw
|
|
levels_raw=$(get_topo_levels "$input_name" ${for_input:+--for "$for_input"}) || true
|
|
|
|
if [[ -z "$levels_raw" ]]; then
|
|
$json && echo '{"dep":"'"$input_name"'","levels":[],"totalRepos":0,"totalLevels":0}' \
|
|
|| echo -e " ${YELLOW}No dependents found for${NC} ${BOLD}$input_name${NC}"
|
|
return
|
|
fi
|
|
|
|
# Count totals
|
|
local max_level=0 total_repos=0
|
|
while IFS='|' read -r lvl _r _d; do
|
|
[[ "$lvl" -gt "$max_level" ]] && max_level=$lvl
|
|
((total_repos++)) || true
|
|
done <<< "$levels_raw"
|
|
|
|
if $json; then
|
|
local out='{"dep":"'"$input_name"'"'
|
|
[[ -n "$for_input" ]] && out+=',"for":"'"$for_input"'"'
|
|
out+=',"totalRepos":'"$total_repos"',"totalLevels":'"$max_level"',"levels":['
|
|
local prev_lvl=-1 first_level=true first_repo=true
|
|
while IFS='|' read -r lvl repo deps; do
|
|
if [[ "$lvl" -ne "$prev_lvl" ]]; then
|
|
[[ "$prev_lvl" -ge 0 ]] && out+=']}'
|
|
$first_level && first_level=false || out+=','
|
|
out+='{"level":'"$lvl"',"repos":['
|
|
prev_lvl=$lvl first_repo=true
|
|
fi
|
|
$first_repo && first_repo=false || out+=','
|
|
out+='{"repo":"'"$repo"'","changedDeps":['
|
|
local first_dep=true
|
|
IFS=',' read -ra dep_arr <<< "$deps"
|
|
for d in "${dep_arr[@]}"; do
|
|
[[ -z "$d" ]] && continue
|
|
$first_dep && first_dep=false || out+=','
|
|
out+='"'"$d"'"'
|
|
done
|
|
out+=']}'
|
|
done <<< "$levels_raw"
|
|
[[ "$prev_lvl" -ge 0 ]] && out+=']}'
|
|
out+=']}'
|
|
echo "$out"
|
|
return
|
|
fi
|
|
|
|
# Human-readable output
|
|
echo ""
|
|
local header="${BOLD}Update order for ${CYAN}$input_name${NC}"
|
|
[[ -n "$for_input" ]] && header+=" ${BOLD}(scoped to $for_input)${NC}"
|
|
echo -e "$header"
|
|
echo ""
|
|
|
|
local prev_lvl=-1
|
|
while IFS='|' read -r lvl repo deps; do
|
|
if [[ "$lvl" -ne "$prev_lvl" ]]; then
|
|
[[ "$prev_lvl" -ge 0 ]] && echo ""
|
|
if [[ "$lvl" -eq 1 ]]; then
|
|
echo -e " ${BOLD}Level $lvl${NC} ${YELLOW}(update + push these first):${NC}"
|
|
else
|
|
echo -e " ${BOLD}Level $lvl${NC} ${YELLOW}(after level $(( lvl - 1 )) is pushed):${NC}"
|
|
fi
|
|
prev_lvl=$lvl
|
|
fi
|
|
local dep_display="${deps//,/, }"
|
|
printf " %-40s ${YELLOW}(depends on: %s)${NC}\n" "$repo" "$dep_display"
|
|
if $commands; then
|
|
local repo_dir
|
|
repo_dir=$(get_repo_path "$repo")
|
|
local update_flags=""
|
|
IFS=',' read -ra dep_arr <<< "$deps"
|
|
for d in "${dep_arr[@]}"; do
|
|
[[ -z "$d" ]] && continue
|
|
update_flags+=" --update-input $d"
|
|
done
|
|
echo -e " ${BLUE}cd repos/$repo_dir${NC}"
|
|
echo -e " ${BLUE}nix flake lock --extra-experimental-features 'nix-command flakes'$update_flags${NC}"
|
|
echo -e " ${BLUE}git add flake.lock && git commit -m 'update $input_name' && git push${NC}"
|
|
echo ""
|
|
fi
|
|
done <<< "$levels_raw"
|
|
|
|
echo ""
|
|
local repo_word="repos" level_word="levels"
|
|
[[ "$total_repos" -eq 1 ]] && repo_word="repo"
|
|
[[ "$max_level" -eq 1 ]] && level_word="level"
|
|
echo -e " ${BOLD}Total:${NC} $total_repos $repo_word across $max_level $level_word"
|
|
}
|
|
|
|
cmd_nix_diagnose() {
|
|
wants_help "$@" && show_help "Usage: ws nix-diagnose <repo> [--suggest]
|
|
|
|
Diagnose dependency issues in a repo's flake.
|
|
|
|
Checks:
|
|
1. Circular dependencies in the workspace dependency graph
|
|
2. Duplicate dependencies — same repo appearing at different versions
|
|
in the flake.lock (e.g. two copies of logos-nix at different revs)
|
|
|
|
Options:
|
|
--suggest After showing conflicts, rank them by impact and suggest
|
|
the best starting point with ws update-order
|
|
|
|
Examples:
|
|
ws nix-diagnose logos-basecamp
|
|
ws nix-diagnose logos-basecamp --suggest
|
|
ws nix-diagnose logos-liblogos"
|
|
|
|
local repo="" suggest=false
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--suggest) suggest=true; shift ;;
|
|
-*) shift ;;
|
|
*) repo="$1"; shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$repo" ]]; then
|
|
echo -e "${RED}Error:${NC} repo name required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local entry
|
|
entry=$(find_repo_entry "$repo") || { echo -e "${RED}Unknown repo:${NC} $repo" >&2; exit 1; }
|
|
local input_name dir_name
|
|
input_name=$(get_field "$entry" 1)
|
|
dir_name=$(get_field "$entry" 2)
|
|
|
|
# ── 1. Circular dependency detection (from flake.lock) ─────────────────────
|
|
log "${BOLD}Circular Dependencies${NC}"
|
|
|
|
local lock_file="$REPOS_DIR/$dir_name/flake.lock"
|
|
if [[ ! -f "$lock_file" ]]; then
|
|
echo -e " ${YELLOW}No flake.lock found, skipping${NC}"
|
|
elif ! command -v jq &>/dev/null; then
|
|
echo -e " ${RED}jq is required but not found${NC}" >&2
|
|
else
|
|
# Build a repo-level dependency graph from the flake.lock.
|
|
# Each lock node maps to a repo identity (original.repo or the node key
|
|
# with _N suffixes stripped). Edges are the direct (non-follows) inputs.
|
|
# Output: "source_repo target_repo" lines (unique).
|
|
local repo_edges
|
|
repo_edges=$(jq -r '
|
|
.nodes as $nodes |
|
|
[ $nodes | to_entries[] |
|
|
select(.value.inputs != null) |
|
|
.key as $parent |
|
|
( $nodes[$parent].original.repo // ($parent | sub("_[0-9]+$"; "")) ) as $src |
|
|
.value.inputs | to_entries[] |
|
|
select(.value | type == "string") |
|
|
.value as $child |
|
|
( $nodes[$child].original.repo // ($child | sub("_[0-9]+$"; "")) ) as $dst |
|
|
select($src != $dst) |
|
|
$src + " " + $dst
|
|
] | unique[]
|
|
' "$lock_file" 2>/dev/null || true)
|
|
|
|
if [[ -z "$repo_edges" ]]; then
|
|
echo -e " ${GREEN}No circular dependencies found${NC}"
|
|
else
|
|
# Build adjacency list as "repo|dep1,dep2,..." lines
|
|
local graph_data
|
|
graph_data=$(echo "$repo_edges" | awk '{
|
|
deps[$1] = deps[$1] ? deps[$1] "," $2 : $2
|
|
} END {
|
|
for (n in deps) print n "|" deps[n]
|
|
}')
|
|
|
|
# DFS cycle detection using temp files (bash 3.2 compatible)
|
|
local _dfs_state_file _dfs_path_file
|
|
_dfs_state_file=$(mktemp)
|
|
_dfs_path_file=$(mktemp)
|
|
local found_cycle=false
|
|
|
|
_dfs_cycle() {
|
|
local node="$1"
|
|
echo "$node visiting" >> "$_dfs_state_file"
|
|
echo "$node" >> "$_dfs_path_file"
|
|
|
|
local deps_str
|
|
deps_str=$(echo "$graph_data" | grep "^${node}|" | head -1 | cut -d'|' -f2 || true)
|
|
local dep
|
|
for dep in ${deps_str//,/ }; do
|
|
[[ -z "$dep" ]] && continue
|
|
local state
|
|
state=$(grep "^${dep} " "$_dfs_state_file" 2>/dev/null | tail -1 | cut -d' ' -f2 || true)
|
|
if [[ "$state" == "visiting" ]]; then
|
|
found_cycle=true
|
|
local cycle="" in_cycle=false
|
|
while IFS= read -r p; do
|
|
[[ "$p" == "$dep" ]] && in_cycle=true
|
|
if $in_cycle; then
|
|
[[ -n "$cycle" ]] && cycle+=" -> "
|
|
cycle+="$p"
|
|
fi
|
|
done < "$_dfs_path_file"
|
|
echo -e " ${RED}Cycle:${NC} $cycle -> $dep"
|
|
elif [[ -z "$state" ]]; then
|
|
_dfs_cycle "$dep"
|
|
fi
|
|
done
|
|
|
|
# Pop path (GNU sed uses -i, BSD sed requires -i '')
|
|
if sed -i '$ d' "$_dfs_path_file" 2>/dev/null; then :; else sed -i '' '$ d' "$_dfs_path_file"; fi
|
|
echo "$node done" >> "$_dfs_state_file"
|
|
}
|
|
|
|
# Start DFS from the repo's own identity
|
|
local start_repo
|
|
start_repo=$(jq -r '
|
|
.nodes.root.inputs | to_entries[] |
|
|
select(.value | type == "string") | .key
|
|
' "$lock_file" 2>/dev/null | head -1 || true)
|
|
# Visit all repos reachable from root inputs
|
|
local root_inputs
|
|
root_inputs=$(jq -r '
|
|
.nodes.root.inputs | to_entries[] |
|
|
select(.value | type == "string") | .value
|
|
' "$lock_file" 2>/dev/null || true)
|
|
local repo_name
|
|
for repo_name in $root_inputs; do
|
|
local identity
|
|
identity=$(jq -r --arg n "$repo_name" '
|
|
.nodes[$n].original.repo // ($n | sub("_[0-9]+$"; ""))
|
|
' "$lock_file" 2>/dev/null || true)
|
|
[[ -z "$identity" ]] && continue
|
|
local state
|
|
state=$(grep "^${identity} " "$_dfs_state_file" 2>/dev/null | tail -1 | cut -d' ' -f2 || true)
|
|
[[ -z "$state" ]] && _dfs_cycle "$identity"
|
|
done
|
|
|
|
rm -f "$_dfs_state_file" "$_dfs_path_file"
|
|
|
|
if ! $found_cycle; then
|
|
echo -e " ${GREEN}No circular dependencies found${NC}"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ── 2. Dependency version conflicts ─────────────────────────────────────────
|
|
log ""
|
|
log "${BOLD}Dependency Version Conflicts${NC}"
|
|
|
|
local lock_file="$REPOS_DIR/$dir_name/flake.lock"
|
|
if [[ ! -f "$lock_file" ]]; then
|
|
echo -e " ${YELLOW}No flake.lock found${NC}"
|
|
return
|
|
fi
|
|
|
|
if ! command -v jq &>/dev/null; then
|
|
echo -e " ${RED}jq is required but not found${NC}" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Find groups of nodes that map to the same repo but different revs
|
|
local conflicts_json
|
|
conflicts_json=$(jq '
|
|
.nodes as $nodes |
|
|
[
|
|
$nodes | to_entries[] |
|
|
select(.key != "root") |
|
|
select(.value.locked != null) |
|
|
select(.value.original.type == "github") |
|
|
{
|
|
key: .key,
|
|
repo_id: (.value.original.owner + "/" + .value.original.repo),
|
|
rev: .value.locked.rev
|
|
}
|
|
] | group_by(.repo_id) |
|
|
map(select([.[].rev] | unique | length > 1))
|
|
' "$lock_file" 2>/dev/null)
|
|
|
|
local conflict_count
|
|
conflict_count=$(echo "$conflicts_json" | jq 'length')
|
|
|
|
if [[ "$conflict_count" == "0" ]]; then
|
|
echo -e " ${GREEN}No version conflicts found${NC}"
|
|
return
|
|
fi
|
|
|
|
# Build adjacency list (one jq call): "parent child input_name" per line
|
|
local adj_lines
|
|
adj_lines=$(jq -r '
|
|
.nodes | to_entries[] |
|
|
.key as $parent |
|
|
select(.value.inputs != null) |
|
|
.value.inputs | to_entries[] |
|
|
select(.value | type == "string") |
|
|
$parent + " " + .value + " " + .key
|
|
' "$lock_file" 2>/dev/null)
|
|
|
|
# BFS to find shortest path from root to a target node (bash 3.2 compatible)
|
|
_find_path() {
|
|
local target="$1"
|
|
[[ "$target" == "root" ]] && { echo "root"; return; }
|
|
|
|
local _bfs_queue_file _bfs_seen_file
|
|
_bfs_queue_file=$(mktemp)
|
|
_bfs_seen_file=$(mktemp)
|
|
echo "root|root" > "$_bfs_queue_file"
|
|
|
|
local result="(unreachable)"
|
|
while [[ -s "$_bfs_queue_file" ]]; do
|
|
local item
|
|
item=$(head -1 "$_bfs_queue_file")
|
|
if sed -i '1d' "$_bfs_queue_file" 2>/dev/null; then :; else sed -i '' '1d' "$_bfs_queue_file"; fi
|
|
|
|
local node="${item%%|*}"
|
|
local path="${item#*|}"
|
|
|
|
grep -qx "$node" "$_bfs_seen_file" 2>/dev/null && continue
|
|
echo "$node" >> "$_bfs_seen_file"
|
|
|
|
if [[ "$node" == "$target" ]]; then
|
|
result="$path"
|
|
break
|
|
fi
|
|
|
|
while IFS=' ' read -r p c i; do
|
|
if [[ "$p" == "$node" ]] && ! grep -qx "$c" "$_bfs_seen_file" 2>/dev/null; then
|
|
echo "$c|$path -> $i" >> "$_bfs_queue_file"
|
|
fi
|
|
done <<< "$adj_lines"
|
|
done
|
|
|
|
rm -f "$_bfs_queue_file" "$_bfs_seen_file"
|
|
echo "$result"
|
|
}
|
|
|
|
echo -e " ${YELLOW}Found $conflict_count conflict(s):${NC}"
|
|
echo ""
|
|
|
|
local i=0
|
|
while [[ $i -lt $conflict_count ]]; do
|
|
local repo_id
|
|
repo_id=$(echo "$conflicts_json" | jq -r ".[$i][0].repo_id")
|
|
echo -e " ${BOLD}$repo_id${NC}:"
|
|
|
|
# Group by rev within this conflict, output "short_rev|full_rev|key1,key2,..."
|
|
echo "$conflicts_json" | jq -r --argjson idx "$i" '
|
|
.[$idx] | group_by(.rev) | .[] |
|
|
.[0].rev[0:7] + "|" + .[0].rev + "|" + ([.[].key] | join(","))
|
|
' | while IFS='|' read -r short_rev _full_rev node_keys; do
|
|
echo -e " rev ${CYAN}$short_rev${NC}:"
|
|
IFS=',' read -ra keys <<< "$node_keys"
|
|
for key in "${keys[@]}"; do
|
|
local path
|
|
path=$(_find_path "$key")
|
|
echo " $path"
|
|
done
|
|
done
|
|
|
|
echo ""
|
|
((i++))
|
|
done
|
|
|
|
# ── Suggested Resolution Order (--suggest) ────────────────────────────────
|
|
if $suggest; then
|
|
log ""
|
|
log "${BOLD}Suggested Resolution Order${NC}"
|
|
|
|
# Rank conflicts: score = rev_count * 1000 + total_lock_refs
|
|
# More distinct revisions = more divergence = higher priority
|
|
# Skip infrastructure-only deps (logos-nix, nixpkgs) — they're not actionable
|
|
local ranked
|
|
ranked=$(echo "$conflicts_json" | jq -r '
|
|
.[] | . as $group |
|
|
($group[0].repo_id) as $repo_id |
|
|
select($repo_id | test("logos-nix|nixpkgs") | not) |
|
|
($group | length) as $total_refs |
|
|
($group | map(.rev) | unique | length) as $rev_count |
|
|
"\($rev_count * 1000 + $total_refs)|\($rev_count)|\($total_refs)|\($repo_id)"
|
|
' | sort -t'|' -k1,1rn)
|
|
|
|
echo ""
|
|
if [[ -z "$ranked" ]]; then
|
|
echo -e " ${YELLOW}All conflicts are in infrastructure deps (logos-nix/nixpkgs) — no actionable suggestions.${NC}"
|
|
else
|
|
echo -e " ${BOLD}Conflicts ranked by impact (excluding logos-nix/nixpkgs):${NC}"
|
|
while IFS='|' read -r _score rev_count total_refs repo_id; do
|
|
local ws_input
|
|
ws_input=$(_github_repo_to_input_name "$repo_id")
|
|
local label="${ws_input:-$repo_id}"
|
|
echo -e " ${CYAN}$label${NC} ($rev_count revisions, $total_refs lock references)"
|
|
done <<< "$ranked"
|
|
|
|
# Show update-order for the most impactful conflict
|
|
local top_repo_id
|
|
top_repo_id=$(echo "$ranked" | head -1 | cut -d'|' -f4)
|
|
local top_input
|
|
top_input=$(_github_repo_to_input_name "$top_repo_id")
|
|
|
|
if [[ -n "$top_input" ]]; then
|
|
echo ""
|
|
echo -e " ${BOLD}Start with:${NC} ${CYAN}ws update-order $top_input --for $input_name${NC}"
|
|
echo ""
|
|
local levels_raw
|
|
levels_raw=$(get_topo_levels "$top_input" --for "$input_name") || true
|
|
if [[ -n "$levels_raw" ]]; then
|
|
local lv_max=0 lv_total=0
|
|
while IFS='|' read -r lvl _r _d; do
|
|
[[ "$lvl" -gt "$lv_max" ]] && lv_max=$lvl
|
|
((lv_total++)) || true
|
|
done <<< "$levels_raw"
|
|
echo -e " This affects $lv_total repos across $lv_max levels:"
|
|
local prev_lvl=-1 level_repos=""
|
|
while IFS='|' read -r lvl repo _deps; do
|
|
if [[ "$lvl" -ne "$prev_lvl" ]]; then
|
|
[[ -n "$level_repos" ]] && echo -e " $prev_lvl. $level_repos"
|
|
level_repos="$repo"
|
|
prev_lvl=$lvl
|
|
else
|
|
level_repos="$level_repos, $repo"
|
|
fi
|
|
done <<< "$levels_raw"
|
|
[[ -n "$level_repos" ]] && echo -e " $prev_lvl. $level_repos"
|
|
fi
|
|
fi
|
|
fi
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
cmd_repo_upgrade() {
|
|
wants_help "$@" && show_help "Usage: ws repo-upgrade [repo...|--group name|--all]
|
|
|
|
Pull the latest master/main for repo submodules.
|
|
|
|
Without arguments, upgrades ALL repos. With repo names, upgrades only those.
|
|
Fetches from origin and checks out the latest commit on the default branch
|
|
(master or main). Retries on failure (20s, then 40s).
|
|
|
|
Options:
|
|
--group, -g <name...> Use a named repo group (see 'ws groups')
|
|
--all Upgrade all repos (default when no args given)
|
|
|
|
Examples:
|
|
ws repo-upgrade Upgrade all repos
|
|
ws repo-upgrade logos-cpp-sdk Upgrade a single repo
|
|
ws repo-upgrade logos-cpp-sdk logos-module Upgrade multiple repos
|
|
ws repo-upgrade --group core Upgrade all core repos"
|
|
|
|
local targets=()
|
|
local all=false
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--group|-g)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local _gexp; _gexp=$(expand_group "$1") || exit 1
|
|
while IFS= read -r r; do [[ -n "$r" ]] && targets+=("$r"); done <<< "$_gexp"
|
|
shift
|
|
done ;;
|
|
--all) all=true; shift ;;
|
|
-*) shift ;;
|
|
*) targets+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
# Default to all repos when no targets specified
|
|
if [[ ${#targets[@]} -eq 0 ]]; then
|
|
all=true
|
|
fi
|
|
|
|
local entries=()
|
|
if $all; then
|
|
entries=("${REPOS[@]}")
|
|
else
|
|
for t in "${targets[@]}"; do
|
|
local entry
|
|
entry=$(find_repo_entry "$t") || { echo -e "${RED}Unknown repo:${NC} $t" >&2; continue; }
|
|
entries+=("$entry")
|
|
done
|
|
fi
|
|
|
|
local failed=()
|
|
local total=${#entries[@]}
|
|
local i=0
|
|
|
|
for entry in "${entries[@]}"; do
|
|
local dir_name
|
|
dir_name=$(get_field "$entry" 2)
|
|
local repo_dir="$REPOS_DIR/$dir_name"
|
|
((i++))
|
|
|
|
if [[ ! -e "$repo_dir/.git" ]]; then
|
|
log " ${YELLOW}[$i/$total] $dir_name — not cloned, skipping${NC}"
|
|
continue
|
|
fi
|
|
|
|
local default_branch
|
|
default_branch=$(git -C "$repo_dir" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@refs/remotes/origin/@@' || echo "master")
|
|
|
|
local fetched=false
|
|
for wait in 0 20 40; do
|
|
[[ "$wait" -gt 0 ]] && log " ${YELLOW}retrying in ${wait}s...${NC}" && sleep "$wait"
|
|
if git -C "$repo_dir" fetch origin "$default_branch" 2>/dev/null; then
|
|
fetched=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
if $fetched; then
|
|
git -C "$repo_dir" checkout "origin/$default_branch" --detach 2>/dev/null
|
|
local short_rev
|
|
short_rev=$(git -C "$repo_dir" rev-parse --short HEAD 2>/dev/null)
|
|
log " ${GREEN}[$i/$total] $dir_name${NC} -> ${CYAN}$default_branch${NC} ($short_rev)"
|
|
else
|
|
log " ${RED}[$i/$total] $dir_name — fetch failed${NC}"
|
|
failed+=("$dir_name")
|
|
fi
|
|
|
|
sleep $((RANDOM % 2 + 2))
|
|
done
|
|
|
|
echo ""
|
|
if [[ ${#failed[@]} -gt 0 ]]; then
|
|
echo -e "${RED}Failed repos:${NC}"
|
|
printf " %s\n" "${failed[@]}"
|
|
else
|
|
log "${GREEN}All ${total} repos upgraded.${NC}"
|
|
fi
|
|
}
|
|
|
|
cmd_check_qt() {
|
|
wants_help "$@" && show_help "Usage: ws check-qt <repo>
|
|
|
|
Check a repo's build for Qt version conflicts that cause runtime crashes
|
|
('Class QT_ROOT_LEVEL_POOL ... is implemented in both ...').
|
|
|
|
Checks two things:
|
|
1. Whether the repo's nix closure has a DIRECT dependency on multiple Qt
|
|
versions (broken follows chain — nix-level fix needed).
|
|
2. Whether the user's local module cache contains modules built against a
|
|
different Qt than the current build (stale cache — clear the data dir).
|
|
|
|
Examples:
|
|
ws check-qt logos-basecamp
|
|
ws check-qt logos-liblogos"
|
|
|
|
local repo="${1:?Usage: ws check-qt <repo>}"
|
|
local input_name
|
|
input_name=$(get_input_name "$repo")
|
|
if [[ -z "$input_name" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo' has no flake input" >&2
|
|
exit 1
|
|
fi
|
|
|
|
log "${BOLD}Checking Qt versions for ${CYAN}$input_name${NC}${BOLD}...${NC}"
|
|
|
|
local store_path
|
|
store_path=$(nix build --extra-experimental-features "nix-command flakes" \
|
|
--no-link --print-out-paths "$WORKSPACE_ROOT#$input_name" 2>/dev/null)
|
|
if [[ -z "$store_path" ]]; then
|
|
echo -e "${RED}Error:${NC} failed to build $input_name" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Find all qtbase store paths in the transitive closure
|
|
local all_qt_paths
|
|
all_qt_paths=$(nix-store -qR "$store_path" 2>/dev/null | grep 'qtbase-[0-9]' || true)
|
|
if [[ -z "$all_qt_paths" ]]; then
|
|
echo -e "${GREEN}OK:${NC} No Qt found in closure (non-Qt repo)"
|
|
return 0
|
|
fi
|
|
|
|
# Use nix why-depends to check which Qt versions the build DIRECTLY depends on.
|
|
# External deps (logos-chat, logos-delivery, etc.) may bring their own nixpkgs
|
|
# with a different qtbase transitively, but that's harmless if it's not loaded
|
|
# by the app at runtime.
|
|
local direct_qt=()
|
|
while IFS= read -r qt_path; do
|
|
if nix why-depends --extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT#$input_name" "$qt_path" >/dev/null 2>&1; then
|
|
local ver
|
|
ver=$(echo "$qt_path" | grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+')
|
|
direct_qt+=("$ver")
|
|
fi
|
|
done <<< "$all_qt_paths"
|
|
|
|
# Deduplicate
|
|
local unique_qt
|
|
unique_qt=$(printf '%s\n' "${direct_qt[@]}" | sort -u)
|
|
local qt_count
|
|
qt_count=$(echo "$unique_qt" | grep -c . 2>/dev/null || true)
|
|
|
|
if [[ "$qt_count" -gt 1 ]]; then
|
|
echo -e "${RED}CONFLICT:${NC} Build directly depends on multiple Qt versions:"
|
|
echo "$unique_qt" | while read -r v; do echo -e " ${YELLOW}*${NC} $v"; done
|
|
echo ""
|
|
echo -e "This ${BOLD}will crash${NC} at runtime. Fix the follows chain in flake.nix."
|
|
echo -e "Trace each version with:"
|
|
while IFS= read -r qt_path; do
|
|
local ver
|
|
ver=$(echo "$qt_path" | grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+')
|
|
if nix why-depends --extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT#$input_name" "$qt_path" >/dev/null 2>&1; then
|
|
echo -e " ${BOLD}nix why-depends .#$input_name $qt_path${NC}"
|
|
fi
|
|
done <<< "$all_qt_paths"
|
|
exit 1
|
|
elif [[ "$qt_count" -eq 1 ]]; then
|
|
echo -e "${GREEN}OK:${NC} Single Qt version in direct deps: $unique_qt"
|
|
fi
|
|
|
|
# Note transitive Qt versions (informational, not a build error)
|
|
local all_qt_versions
|
|
all_qt_versions=$(echo "$all_qt_paths" | grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+' | sort -u)
|
|
local all_qt_count
|
|
all_qt_count=$(echo "$all_qt_versions" | grep -c . 2>/dev/null || true)
|
|
if [[ "$all_qt_count" -gt 1 ]]; then
|
|
log ""
|
|
log "${YELLOW}Note:${NC} Transitive closure contains other Qt versions (from external deps):"
|
|
echo "$all_qt_versions" | while read -r v; do
|
|
if [[ "$unique_qt" != *"$v"* ]]; then
|
|
log " ${YELLOW}*${NC} $v (indirect — not loaded by $input_name)"
|
|
fi
|
|
done
|
|
log "This is harmless unless modules built against these versions are loaded at runtime."
|
|
fi
|
|
|
|
# Check local module cache for stale Qt
|
|
local app_qt
|
|
app_qt=$(echo "$unique_qt" | head -1)
|
|
if [[ -n "$app_qt" ]]; then
|
|
local cache_dirs=()
|
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
cache_dirs+=("$HOME/Library/Application Support/LogosBasecampNix/modules")
|
|
cache_dirs+=("$HOME/Library/Application Support/LogosBasecampNix/plugins")
|
|
cache_dirs+=("$HOME/Library/Application Support/LogosBasecamp/modules")
|
|
cache_dirs+=("$HOME/Library/Application Support/LogosBasecamp/plugins")
|
|
else
|
|
local xdg="${XDG_DATA_HOME:-$HOME/.local/share}"
|
|
cache_dirs+=("$xdg/LogosBasecampNix/modules" "$xdg/LogosBasecampNix/plugins")
|
|
cache_dirs+=("$xdg/LogosBasecamp/modules" "$xdg/LogosBasecamp/plugins")
|
|
fi
|
|
|
|
local stale_found=false
|
|
for dir in "${cache_dirs[@]}"; do
|
|
[[ -d "$dir" ]] || continue
|
|
local cached_qt
|
|
cached_qt=$(find "$dir" -type f \( -name "*.dylib" -o -name "*.so" \) -exec strings {} + 2>/dev/null \
|
|
| grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+' | sort -u || true)
|
|
if [[ -n "$cached_qt" && "$cached_qt" != *"$app_qt"* ]]; then
|
|
if ! $stale_found; then
|
|
echo ""
|
|
echo -e "${RED}STALE MODULE CACHE:${NC} Local modules reference a different Qt:"
|
|
stale_found=true
|
|
fi
|
|
echo -e " ${YELLOW}$dir${NC} → $cached_qt (build uses $app_qt)"
|
|
fi
|
|
done
|
|
|
|
if $stale_found; then
|
|
echo ""
|
|
echo -e "${YELLOW}Fix:${NC} Clear the stale cache:"
|
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
echo -e " ${BOLD}rm -rf ~/Library/Application\\ Support/LogosBasecampNix${NC}"
|
|
else
|
|
echo -e " ${BOLD}rm -rf \${XDG_DATA_HOME:-~/.local/share}/LogosBasecampNix${NC}"
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
if ! $stale_found; then
|
|
echo -e "${GREEN}OK:${NC} No stale modules in local cache"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
cmd_groups() {
|
|
wants_help "$@" && show_help "Usage: ws groups
|
|
|
|
List all defined repo groups and their members.
|
|
|
|
Examples:
|
|
ws groups"
|
|
|
|
for entry in "${REPO_GROUPS[@]}"; do
|
|
local gname members
|
|
gname=$(echo "$entry" | cut -d'|' -f1)
|
|
members=$(echo "$entry" | cut -d'|' -f2)
|
|
|
|
echo -e "${BOLD}${CYAN}$gname${NC}"
|
|
|
|
local expanded
|
|
expanded=$(expand_group "$gname" | sort -u)
|
|
while IFS= read -r repo; do
|
|
[[ -n "$repo" ]] && echo -e " $repo"
|
|
done <<< "$expanded"
|
|
echo ""
|
|
done
|
|
}
|
|
|
|
cmd_help() {
|
|
cat <<'EOF'
|
|
Logos Workspace CLI
|
|
|
|
USAGE:
|
|
ws [--quiet] [--verbose] <command> [args]
|
|
|
|
GLOBAL OPTIONS:
|
|
--quiet, -q Suppress informational output (for CI/scripting)
|
|
--verbose, -v Show full nix output (default: quiet, errors shown on failure)
|
|
|
|
COMMANDS:
|
|
init [jobs] Clone all submodules (default: 4 parallel jobs)
|
|
list List all repos and their status
|
|
groups List all defined repo groups
|
|
|
|
build <repo...|--group G> [opts] Build one or more repos via nix
|
|
--group, -g <name...> Use a named repo group (see 'ws groups')
|
|
--auto-local, -a Auto-detect dirty repos and use as overrides
|
|
--local, -l <r1> <r2> ... Use specific local repos as overrides
|
|
|
|
bundle <repo...|--group G> [opts] Bundle repos into .lgx packages
|
|
--group, -g <name...> Use a named repo group (see 'ws groups')
|
|
--auto-local, -a Auto-detect dirty repos and use as overrides
|
|
--local, -l <r1> <r2> ... Use specific local repos as overrides
|
|
--mode, -m <dev|portable|dual> Bundle mode (default: dev)
|
|
-o, --out-dir <path> Output dir for multi-repo (default: result-bundle)
|
|
|
|
run <repo> [opts] Build and run a repo (single repo only)
|
|
|
|
run <repo> [opts] [-- <args>] Build and run a repo (single repo only)
|
|
--auto-local, -a Auto-detect dirty repos and use as overrides
|
|
--local, -l <r1> <r2> ... Use specific local repos as overrides
|
|
logos-standalone-app only (after --):
|
|
<ui-plugin-repo> Bundle repo → install to ./plugins; or pass existing path
|
|
--modules, -M <repo>[,...] Bundle backend module(s) → install to ./modules
|
|
|
|
develop [repo...|--group G] [opts] Enter a nix devShell
|
|
--group, -g <name...> Use a named repo group
|
|
--auto-local, -a Include overrides for dirty repos
|
|
--local, -l <r1> <r2> ... Include specific local overrides
|
|
--fresh Force rebuild (ignore cache)
|
|
|
|
test [repo...|--group G|--all] [--type T] Run checks/tests
|
|
--group, -g <name...> Use a named repo group
|
|
--type, -t <type> Filter by: cpp, rust, nim, js, qml
|
|
|
|
status Git status across all repos
|
|
dirty Show dirty repos and what they affect
|
|
graph [repo...|--group G] Show dependency graph (DOT format, or repo details)
|
|
override-inputs <repo...|--group G> [opts] Show the nix --override-input flags
|
|
update [repo...|--group G|--all] Update flake.lock inputs
|
|
loc [repo...|--group G|--all] [--no-nix] Count lines of code (uses tokei)
|
|
watch test <repo...|--group G> Re-run tests on file changes
|
|
watch build <repo...|--group G> Re-build on file changes
|
|
watch run <cmd> [-w repo...] Run command on file changes
|
|
foreach <command> Run a command in every repo directory
|
|
worktree add <name> [-b br] Create a worktree with submodules and ws/<branch> branches
|
|
worktree list List all worktrees
|
|
worktree remove <name> Remove a worktree
|
|
repo-upgrade [repo...|--group G|--all] Pull latest master/main for submodules
|
|
sync-graph Regenerate nix/dep-graph.nix from repo flake.nix files
|
|
nix-diagnose <repo> [--suggest] Detect circular deps and version conflicts in flake.lock
|
|
update-order <dep> [--for target] [--json] [--commands]
|
|
Show topological update order for a dependency change
|
|
check-qt <repo> Check for Qt version conflicts in a repo's nix closure
|
|
|
|
lm <args> Module inspector (auto-builds logos-module)
|
|
logoscore <args> Headless runtime (auto-builds logos-liblogos)
|
|
lgx <args> Package tool (auto-builds logos-package)
|
|
lgpd <args> Package downloader (auto-builds logos-package-downloader)
|
|
lgpm <args> Package manager (Portable) (auto-builds logos-package-manager#cli-portable)
|
|
lgpm-dev <args> Package manager (Development) (auto-builds logos-package-manager#cli)
|
|
logos-cpp-generator <args> SDK code generator (auto-builds logos-cpp-sdk)
|
|
|
|
help Show this help
|
|
|
|
REPO GROUPS:
|
|
Most commands accept --group/-g to operate on a named set of repos.
|
|
Groups are defined in the ws script and can reference other groups.
|
|
Use 'ws groups' to see all available groups and their members.
|
|
|
|
EXAMPLES:
|
|
# Set up the workspace
|
|
ws init
|
|
|
|
# Build a single repo
|
|
ws build logos-basecamp --auto-local
|
|
|
|
# Build all repos in a group
|
|
ws build --group core
|
|
|
|
# Build multiple repos
|
|
ws build logos-module logos-liblogos --auto-local
|
|
|
|
# Explicitly override specific deps
|
|
ws build logos-basecamp --local logos-cpp-sdk logos-liblogos
|
|
|
|
# Bundle a module into an .lgx package
|
|
ws bundle logos-chat-module
|
|
|
|
# Bundle with portable mode (self-contained, no nix store deps)
|
|
ws bundle logos-chat-module --mode portable
|
|
|
|
# Bundle multiple repos (results in result-bundle/<repo>/)
|
|
ws bundle logos-chat-module logos-storage-module --auto-local
|
|
|
|
# Run all tests
|
|
ws test --all
|
|
|
|
# Test a group of repos
|
|
ws test --group core --auto-local
|
|
|
|
# Run only C++ repo tests
|
|
ws test --all --type cpp
|
|
|
|
# Test specific repos
|
|
ws test logos-cpp-sdk logos-module
|
|
|
|
# See what your dirty repos affect
|
|
ws dirty
|
|
|
|
# List available groups
|
|
ws groups
|
|
|
|
# Enter logos-basecamp's devShell with local logos-cpp-sdk
|
|
ws develop logos-basecamp --local logos-cpp-sdk
|
|
|
|
# Visualize the dependency graph
|
|
ws graph | dot -Tsvg > graph.svg
|
|
|
|
# See deps of specific repos
|
|
ws graph logos-basecamp logos-liblogos
|
|
|
|
# Run git pull in every repo
|
|
ws foreach git pull
|
|
|
|
# Show override flags without executing
|
|
ws override-inputs logos-basecamp --auto-local
|
|
|
|
# Create an isolated worktree for a feature branch
|
|
ws worktree add my-feature -b my-feature-branch
|
|
# Result: ../logos-workspace--my-feature/ with all repos on ws/my-feature-branch
|
|
|
|
# Remove a worktree when done
|
|
ws worktree remove my-feature
|
|
|
|
# Lines of code across all repos
|
|
ws loc --all --no-nix
|
|
|
|
# Lines of code for a group
|
|
ws loc --group core
|
|
|
|
# Lines of code for specific repos
|
|
ws loc logos-cpp-sdk logos-module
|
|
|
|
# Auto-run tests when files change
|
|
ws watch test logos-module
|
|
|
|
# Watch and test a group
|
|
ws watch test --group core --auto-local
|
|
|
|
# Watch and test multiple repos
|
|
ws watch test logos-module logos-liblogos --auto-local
|
|
|
|
# Auto-build on changes with local overrides
|
|
ws watch build logos-basecamp --auto-local
|
|
|
|
# Watch specific repos and run a custom command
|
|
ws watch run 'ws test logos-module --auto-local' -w logos-module -w logos-cpp-sdk
|
|
|
|
# Regenerate dep-graph.nix after adding tests to a repo
|
|
ws sync-graph
|
|
|
|
# Show what order to update deps when logos-module changes
|
|
ws update-order logos-module
|
|
|
|
# Scope to repos that affect logos-basecamp only
|
|
ws update-order logos-cpp-sdk --for logos-basecamp
|
|
|
|
# Diagnose version conflicts in logos-basecamp + suggest fix order
|
|
ws nix-diagnose logos-basecamp --suggest
|
|
EOF
|
|
}
|
|
|
|
# ── Parse global flags ────────────────────────────────────────────────────────
|
|
_args=()
|
|
for _arg in "$@"; do
|
|
case "$_arg" in
|
|
--quiet|-q) QUIET=true ;;
|
|
--verbose|-v) VERBOSE=true ;;
|
|
*) _args+=("$_arg") ;;
|
|
esac
|
|
done
|
|
set -- "${_args[@]}"
|
|
|
|
if $QUIET; then
|
|
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' NC=''
|
|
fi
|
|
|
|
# ── Main dispatch ────────────────────────────────────────────────────────────
|
|
|
|
case "${1:-help}" in
|
|
init) shift; cmd_init "$@" ;;
|
|
build) shift; cmd_build "$@" ;;
|
|
bundle) shift; cmd_bundle "$@" ;;
|
|
run) shift; cmd_run "$@" ;;
|
|
develop|dev) shift; cmd_develop "$@" ;;
|
|
test) shift; cmd_test "$@" ;;
|
|
status|st) shift; cmd_status "$@" ;;
|
|
dirty) shift; cmd_dirty "$@" ;;
|
|
graph) shift; cmd_graph "$@" ;;
|
|
override-inputs|oi) shift; cmd_override_inputs "$@" ;;
|
|
update) shift; cmd_update "$@" ;;
|
|
loc) shift; cmd_loc "$@" ;;
|
|
watch) shift; cmd_watch "$@" ;;
|
|
foreach) shift; cmd_foreach "$@" ;;
|
|
worktree|wt) shift; cmd_worktree "$@" ;;
|
|
sync-graph) shift; cmd_sync_graph "$@" ;;
|
|
check-qt) shift; cmd_check_qt "$@" ;;
|
|
nix-diagnose) shift; cmd_nix_diagnose "$@" ;;
|
|
update-order) shift; cmd_update_order "$@" ;;
|
|
repo-upgrade) shift; cmd_repo_upgrade "$@" ;;
|
|
list|ls) shift; cmd_list "$@" ;;
|
|
groups) shift; cmd_groups "$@" ;;
|
|
help|--help|-h) cmd_help ;;
|
|
# CLI tool wrappers — delegate to scripts/
|
|
lm|logoscore|lgx|lgpm|lgpm-dev|lgpd|logos-cpp-generator)
|
|
exec "$WORKSPACE_ROOT/scripts/$1" "${@:2}" ;;
|
|
*)
|
|
echo -e "${RED}Unknown command:${NC} $1" >&2
|
|
echo "Run 'ws help' for usage." >&2
|
|
exit 1
|
|
;;
|
|
esac
|