mirror of
https://github.com/logos-co/logos-workspace.git
synced 2026-08-31 12:41:14 +00:00
2977 lines
98 KiB
Bash
Executable File
2977 lines
98 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
|
|
|
|
# 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 "$@"; }
|
|
|
|
|
|
# ── 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-cpp-sdk|logos-cpp-sdk|https://github.com/logos-co/logos-cpp-sdk.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-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-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"
|
|
|
|
# App
|
|
"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-legacy-module|logos-chat-legacy-module|https://github.com/logos-co/logos-chat-legacy-module.git|yes"
|
|
"logos-chat-tui|logos-chat-tui|https://github.com/logos-co/logos-chat-tui.git|yes"
|
|
"logos-chat-ui|logos-chat-ui|https://github.com/logos-co/logos-chat-ui.git|yes"
|
|
"logos-chatsdk-module|logos-chatsdk-module|https://github.com/logos-co/logos-chatsdk-module.git|yes"
|
|
"logos-chatsdk-ui|logos-chatsdk-ui|https://github.com/logos-co/logos-chatsdk-ui.git|yes"
|
|
"logos-irc-module|logos-irc-module|https://github.com/logos-co/logos-irc-module.git|yes"
|
|
|
|
# Waku
|
|
"logos-waku-module|logos-waku-module|https://github.com/logos-co/logos-waku-module.git|yes"
|
|
"logos-waku-ui|logos-waku-ui|https://github.com/logos-co/logos-waku-ui.git|yes"
|
|
|
|
# Package Manager
|
|
"logos-package-manager-module|logos-package-manager-module|https://github.com/logos-co/logos-package-manager-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"
|
|
|
|
# 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"
|
|
|
|
# Testing
|
|
"logos-test-modules|logos-test-modules|https://github.com/logos-co/logos-test-modules.git|yes"
|
|
|
|
# Other
|
|
"logos-modules|logos-modules|https://github.com/logos-co/logos-modules.git|yes"
|
|
"logos-module-viewer|logos-module-viewer|https://github.com/logos-co/logos-module-viewer.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-cpp-sdk,logos-module,logos-liblogos,logos-module-builder,logos-capability-module,logos-package-manager-module,logos-test-modules"
|
|
"chat|core,logos-chat-module,logos-chatsdk-module,logos-waku-module,logos-irc-module"
|
|
"package-manager|logos-package,logos-package-manager-module,logos-package-manager-ui,nix-bundle-lgx"
|
|
"app|core,logos-basecamp,logos-package-manager-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 dir_name="$1"
|
|
for entry in "${REPOS[@]}"; do
|
|
if [[ "$(get_field "$entry" 2)" == "$dir_name" ]]; then
|
|
echo "$(get_field "$entry" 1)"
|
|
return 0
|
|
fi
|
|
done
|
|
echo "$dir_name"
|
|
}
|
|
|
|
# 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
|
|
input_name=$(get_input_name "$dir_name")
|
|
if [[ -n "$input_name" ]]; then
|
|
flags+=("--override-input" "$input_name" "path:$REPOS_DIR/$dir_name")
|
|
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
|
|
}
|
|
|
|
# ── 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}"
|
|
exec nix build \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$nix_ref" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
${extra_args[@]+"${extra_args[@]}"}
|
|
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 ! 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-waku-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}"
|
|
exec nix bundle \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
--bundler "$bundler_ref" \
|
|
-o result \
|
|
"$WORKSPACE_ROOT#$target" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
${extra_args[@]+"${extra_args[@]}"}
|
|
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 ! 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 in a single nix eval call
|
|
_discover_test_outputs() {
|
|
local repo_path="$1"
|
|
local system="$2"
|
|
|
|
# Single eval: get both checks and test-like packages at once
|
|
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":[]}'
|
|
}
|
|
|
|
# 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 (always from repo flake for discovery)
|
|
log " ${BLUE}Discovering test outputs...${NC}"
|
|
local outputs
|
|
outputs=$(_discover_test_outputs "$repo_path" "$system")
|
|
|
|
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 nix 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 nix 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 nix 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
|
|
}
|
|
|
|
cmd_sync_graph() {
|
|
wants_help "$@" && show_help "Usage: ws sync-graph
|
|
|
|
Regenerate nix/dep-graph.nix by scanning each repo's flake.nix for
|
|
workspace dependencies and test outputs.
|
|
|
|
Examples:
|
|
ws sync-graph"
|
|
|
|
log "${BOLD}Syncing nix/dep-graph.nix from repo flake.nix files...${NC}"
|
|
log ""
|
|
|
|
# Build list of workspace input names
|
|
local ws_names=()
|
|
for entry in "${REPOS[@]}"; do
|
|
local iname
|
|
iname=$(get_field "$entry" 1)
|
|
[[ -n "$iname" ]] && ws_names+=("$iname")
|
|
done
|
|
|
|
# Find max name length for alignment
|
|
local max_len=0
|
|
for name in "${ws_names[@]}"; do
|
|
[[ ${#name} -gt $max_len ]] && max_len=${#name}
|
|
done
|
|
|
|
local tmpfile
|
|
tmpfile=$(mktemp)
|
|
|
|
cat > "$tmpfile" << 'HEADER'
|
|
# Dependency graph: flake input name → { deps, hasTests }
|
|
#
|
|
# deps: list of workspace input names this repo depends on
|
|
# (only logos-co deps; external deps like go-wallet-sdk are omitted)
|
|
# 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
|
|
#
|
|
# If you add a `checks` output to a repo's flake.nix, re-run this command
|
|
# so that `ws test --all` picks it up.
|
|
{
|
|
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 flake_file="$REPOS_DIR/$dir_name/flake.nix"
|
|
if [[ ! -f "$flake_file" ]]; then
|
|
log " ${YELLOW}skip${NC} $dir_name (not cloned)"
|
|
continue
|
|
fi
|
|
|
|
# Find deps: which workspace inputs does this flake reference?
|
|
local deps=()
|
|
for ws_name in "${ws_names[@]}"; do
|
|
[[ "$ws_name" == "$input_name" ]] && continue # skip self
|
|
# Match by input name (name.url, name.follows, name = {)
|
|
# OR by URL referencing the repo (github:logos-co/<name>)
|
|
if grep -qE "^[[:space:]]+${ws_name}(\.|[[:space:]]*=)" "$flake_file" 2>/dev/null || \
|
|
grep -qE "logos-co/${ws_name}(\.git)?\"" "$flake_file" 2>/dev/null; then
|
|
deps+=("$ws_name")
|
|
fi
|
|
done
|
|
|
|
# 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
|
|
|
|
# Format deps
|
|
local deps_str="[]"
|
|
if [[ ${#deps[@]} -gt 0 ]]; then
|
|
deps_str="[ $(printf '"%s" ' "${deps[@]}")]"
|
|
fi
|
|
|
|
# Write entry with padding
|
|
local pad
|
|
pad=$(printf "%-${max_len}s" "$input_name")
|
|
echo " ${pad} = { deps = ${deps_str}; hasTests = ${has_tests}; };" >> "$tmpfile"
|
|
|
|
# Console status
|
|
local marker="${GREEN}ok${NC}"
|
|
[[ "$has_tests" == "true" ]] && marker="${GREEN}ok${NC} ${CYAN}(tests)${NC}"
|
|
log " $marker $input_name (${#deps[@]} deps)"
|
|
done
|
|
|
|
echo "}" >> "$tmpfile"
|
|
log ""
|
|
|
|
# Compare and update
|
|
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:${NC}"
|
|
diff --color=auto -u "$DEP_GRAPH" "$tmpfile" || true
|
|
echo ""
|
|
fi
|
|
mv "$tmpfile" "$DEP_GRAPH"
|
|
echo -e "${GREEN}Updated${NC} nix/dep-graph.nix"
|
|
fi
|
|
}
|
|
|
|
cmd_nix_diagnose() {
|
|
wants_help "$@" && show_help "Usage: ws nix-diagnose <repo>
|
|
|
|
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)
|
|
|
|
Examples:
|
|
ws nix-diagnose logos-basecamp
|
|
ws nix-diagnose logos-liblogos"
|
|
|
|
local repo=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-*) 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
|
|
}
|
|
|
|
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] <command> [args]
|
|
|
|
GLOBAL OPTIONS:
|
|
--quiet, -q Suppress informational output (for CI/scripting)
|
|
|
|
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> Detect circular deps and version conflicts in flake.lock
|
|
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)
|
|
lgpm <args> Package manager (auto-builds logos-package-manager-module)
|
|
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-waku-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
|
|
EOF
|
|
}
|
|
|
|
# ── Parse global flags ────────────────────────────────────────────────────────
|
|
_args=()
|
|
for _arg in "$@"; do
|
|
case "$_arg" in
|
|
--quiet|-q) QUIET=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 "$@" ;;
|
|
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|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
|