mirror of
https://codeberg.org/muon/home.git
synced 2026-03-08 03:25:16 +00:00
394 lines
9 KiB
Bash
394 lines
9 KiB
Bash
# Set default PROJECT_ID if not already set
|
|
if [[ -z "$PROJECT_ID" ]]; then
|
|
export PROJECT_ID="mk2-test"
|
|
fi
|
|
|
|
_hr_usage() {
|
|
echo "Usage: hr <command>"
|
|
echo "Commands:"
|
|
echo " switch Switch PROJECT_ID between mk2-test and mk2-prod"
|
|
echo " call Call a Cloud Run service route"
|
|
echo " cf Call a Cloud Function"
|
|
echo " init py Initialize a python devenv environment (git-ignored)"
|
|
echo " freeze Freeze dependencies to requirements.txt"
|
|
}
|
|
|
|
_hr_init_devenv() {
|
|
if [ -f .gitignore ]; then
|
|
cp .gitignore .gitignore.bak
|
|
fi
|
|
|
|
if command -v devenv >/dev/null; then
|
|
devenv init
|
|
echo "Direnv allowed"
|
|
else
|
|
echo "Error: devenv not found in path."
|
|
return 1
|
|
fi
|
|
|
|
if [ -f .gitignore.bak ]; then
|
|
mv .gitignore.bak .gitignore
|
|
elif [ -f .gitignore ]; then
|
|
rm .gitignore
|
|
fi
|
|
}
|
|
|
|
_hr_add_ignores() {
|
|
if git rev-parse --git-dir >/dev/null 2>&1; then
|
|
EXCLUDE_FILE=$(git rev-parse --git-path info/exclude)
|
|
mkdir -p "$(dirname "$EXCLUDE_FILE")"
|
|
|
|
for file in "$@"; do
|
|
if ! grep -Fxq "$file" "$EXCLUDE_FILE" 2>/dev/null; then
|
|
echo "$file" >>"$EXCLUDE_FILE"
|
|
echo "Added $file to local git exclude ($EXCLUDE_FILE)"
|
|
fi
|
|
done
|
|
else
|
|
echo "Warning: Not a git repository. Skipping git ignore setup."
|
|
fi
|
|
}
|
|
|
|
_hr_py_files() {
|
|
cat <<EOF >devenv.nix
|
|
{pkgs, ...}: {
|
|
packages = [ pkgs.google-cloud-sdk pkgs.libpq ];
|
|
|
|
languages.python = {
|
|
enable = true;
|
|
venv.enable = true;
|
|
uv = {
|
|
enable = true;
|
|
sync.enable = false;
|
|
};
|
|
};
|
|
|
|
# We use the named index "google" defined in uv.toml
|
|
env.UV_INDEX_GOOGLE_USERNAME = "oauth2accesstoken";
|
|
env.PROJECT_ID = "mk2-test";
|
|
|
|
enterShell = ''
|
|
if ! gcloud auth print-access-token >/dev/null 2>&1; then
|
|
echo "⚠️ gcloud not authenticated. Run 'gcloud auth login' to access Google Artifact Registry."
|
|
else
|
|
export UV_INDEX_GOOGLE_PASSWORD=\$(gcloud auth print-access-token)
|
|
fi
|
|
uv sync
|
|
'';
|
|
}
|
|
EOF
|
|
|
|
cat <<EOF >uv.toml
|
|
[[index]]
|
|
name = "google"
|
|
url = "https://europe-west1-python.pkg.dev/mk2-prod/python-packages/simple/"
|
|
EOF
|
|
}
|
|
|
|
_hr_rs_files() {
|
|
cat <<EOF >devenv.nix
|
|
{pkgs, ...}: {
|
|
languages.rust = {
|
|
enable = true;
|
|
channel = "stable";
|
|
};
|
|
}
|
|
EOF
|
|
|
|
cat <<EOF >devenv.yaml
|
|
inputs:
|
|
rust-overlay:
|
|
url: github:oxalica/rust-overlay
|
|
inputs:
|
|
nixpkgs:
|
|
follows: nixpkgs
|
|
EOF
|
|
}
|
|
|
|
_hr_cpp_files() {
|
|
cat <<EOF >devenv.nix
|
|
{ pkgs, ... }:
|
|
let
|
|
# Use glibc-compatible static openssl to match system libs
|
|
staticOpenSSL = pkgs.openssl.override { static = true; };
|
|
|
|
# Shim to satisfy CMake looking for "ssl.a"
|
|
compatOpenSSL = pkgs.runCommand "openssl-compat" {} ''
|
|
mkdir -p \$out/lib
|
|
ln -s \${staticOpenSSL.out}/lib/libssl.a \$out/lib/ssl.a
|
|
ln -s \${staticOpenSSL.out}/lib/libcrypto.a \$out/lib/crypto.a
|
|
'';
|
|
in {
|
|
packages = [
|
|
pkgs.cmake
|
|
pkgs.clang-tools
|
|
pkgs.pkg-config
|
|
pkgs.mosquitto
|
|
|
|
staticOpenSSL
|
|
compatOpenSSL
|
|
];
|
|
# Explicitly add lib paths so linker finds -lssl AND ssl.a
|
|
env.LIBRARY_PATH = "\${staticOpenSSL.out}/lib:\${compatOpenSSL}/lib";
|
|
env.CPATH = "\${staticOpenSSL.dev}/include";
|
|
|
|
languages.cplusplus.enable = true;
|
|
}
|
|
EOF
|
|
}
|
|
|
|
_hr_init_base() {
|
|
local name="$1"
|
|
local func="$2"
|
|
shift 2
|
|
local ignores=("$@")
|
|
echo "Initializing $name devenv..."
|
|
|
|
# 1. Init devenv
|
|
_hr_init_devenv
|
|
|
|
# 2. Replace devenv.nix
|
|
"$func"
|
|
|
|
# 3. Add to local git exclude
|
|
IGNORES=(
|
|
".devenv*"
|
|
".direnv"
|
|
"devenv.nix"
|
|
"devenv.yaml"
|
|
"devenv.lock"
|
|
".envrc"
|
|
"${ignores[@]}"
|
|
)
|
|
_hr_add_ignores "${IGNORES[@]}"
|
|
|
|
direnv allow
|
|
}
|
|
|
|
_hr_init_py() {
|
|
IGNORES=(
|
|
"uv.lock"
|
|
"uv.toml"
|
|
)
|
|
_hr_init_base "Python" _hr_py_files "${IGNORES[@]}"
|
|
}
|
|
|
|
_hr_init_rs() {
|
|
_hr_init_base "Rust" _hr_rs_files
|
|
}
|
|
|
|
_hr_init_cpp() {
|
|
_hr_init_base "C++" _hr_cpp_files
|
|
mkdir -p build &&
|
|
cd build &&
|
|
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Release .. &&
|
|
make -j$(nproc) &&
|
|
cp compile_commands.json ..
|
|
}
|
|
|
|
_hr_freeze() {
|
|
local extra_index_url="https://europe-west1-python.pkg.dev/mk2-prod/python-packages/simple/"
|
|
|
|
# Install the auth plugin and keyring CLI
|
|
uv pip install keyrings.google-artifactregistry-auth==1.1.2 keyring
|
|
|
|
# Install project dependencies using the subprocess keyring provider
|
|
uv pip install --no-cache -e ".[test]" --extra-index-url "${extra_index_url}" --keyring-provider subprocess
|
|
|
|
# Generate requirements.txt
|
|
echo "--extra-index-url ${extra_index_url}" >requirements.txt
|
|
uv pip freeze --exclude-editable >>requirements.txt
|
|
}
|
|
|
|
_hr_add_json_field() {
|
|
local json="$1"
|
|
local key="$2"
|
|
local value="$3"
|
|
local jq_opt="--arg"
|
|
|
|
# Check if explicit boolean
|
|
if [[ "$value" == "true" || "$value" == "false" ]]; then
|
|
jq_opt="--argjson"
|
|
# Check if number (integer or float, no leading zeros unless just 0)
|
|
elif [[ "$value" =~ ^-?(0|[1-9][0-9]*)(\.[0-9]+)?$ ]]; then
|
|
jq_opt="--argjson"
|
|
# Check if object or array
|
|
elif [[ "$value" == "["* || "$value" == "{"* ]]; then
|
|
if echo "$value" | jq empty >/dev/null 2>&1; then
|
|
jq_opt="--argjson"
|
|
else
|
|
# Warn to stderr, but proceed as string
|
|
echo "Warning: Value for '$key' looks like JSON but is invalid. Treating as string." >&2
|
|
fi
|
|
fi
|
|
|
|
# Apply the value at the path defined by the key (dot-notation supported)
|
|
# paths like items.0.id are converted to ["items", 0, "id"]
|
|
echo "$json" | jq --arg k "$key" $jq_opt v "$value" \
|
|
'setpath($k | split(".") | map(if test("^[0-9]+$") then tonumber else . end); $v)'
|
|
}
|
|
|
|
_hr_call() {
|
|
local route_arg="$1"
|
|
shift
|
|
|
|
if [[ -z "$route_arg" ]]; then
|
|
echo "Usage: hr call <route-name>[/path] <options>"
|
|
return 1
|
|
fi
|
|
|
|
local service_name
|
|
local url_path
|
|
|
|
if [[ "$route_arg" == */* ]]; then
|
|
service_name="${route_arg%%/*}"
|
|
url_path="/${route_arg#*/}"
|
|
else
|
|
service_name="$route_arg"
|
|
url_path=""
|
|
fi
|
|
|
|
local project_number
|
|
if [[ "$PROJECT_ID" == "mk2-prod" ]]; then
|
|
project_number="1013087376822"
|
|
else
|
|
project_number="322048751601"
|
|
fi
|
|
|
|
if ! command -v jq >/dev/null; then
|
|
echo "Error: jq is required but not installed."
|
|
return 1
|
|
fi
|
|
|
|
local json_payload="{}"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
if [[ "$1" == -* ]]; then
|
|
local key="${1#-}"
|
|
if [[ -z "$2" || "$2" == -* ]]; then
|
|
echo "Error: Missing value for option $key"
|
|
return 1
|
|
fi
|
|
local value="$2"
|
|
|
|
json_payload=$(_hr_add_json_field "$json_payload" "$key" "$value")
|
|
shift 2
|
|
else
|
|
echo "Error: Unexpected argument '$1'"
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
local url="https://${service_name}-${project_number}.europe-west1.run.app${url_path}"
|
|
|
|
echo "Calling $url..."
|
|
echo "$json_payload"
|
|
|
|
curl "$url" \
|
|
-H "Authorization: bearer $(gcloud auth print-identity-token)" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$json_payload"
|
|
}
|
|
|
|
_hr_cf() {
|
|
local function_name="$1"
|
|
shift
|
|
|
|
if [[ -z "$function_name" ]]; then
|
|
echo "Usage: hr cf <function-name> <options>"
|
|
return 1
|
|
fi
|
|
|
|
if ! command -v jq >/dev/null; then
|
|
echo "Error: jq is required but not installed."
|
|
return 1
|
|
fi
|
|
|
|
local json_payload="{}"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
if [[ "$1" == -* ]]; then
|
|
local key="${1#-}"
|
|
if [[ -z "$2" || "$2" == -* ]]; then
|
|
echo "Error: Missing value for option $key"
|
|
return 1
|
|
fi
|
|
local value="$2"
|
|
|
|
json_payload=$(_hr_add_json_field "$json_payload" "$key" "$value")
|
|
shift 2
|
|
else
|
|
echo "Error: Unexpected argument '$1'"
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
local url="https://europe-west1-${PROJECT_ID}.cloudfunctions.net/${function_name}"
|
|
|
|
echo "Calling $url..."
|
|
echo "$json_payload"
|
|
|
|
curl "$url" \
|
|
-H "Authorization: bearer $(gcloud auth print-identity-token)" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$json_payload"
|
|
}
|
|
|
|
hr() {
|
|
if [[ $# -eq 0 ]]; then
|
|
_hr_usage
|
|
return 1
|
|
fi
|
|
|
|
local command="$1"
|
|
shift
|
|
|
|
if [[ "$command" == "switch" ]]; then
|
|
if [[ -z "$1" ]]; then
|
|
# Toggle between test and prod
|
|
if [[ "$PROJECT_ID" == "mk2-test" ]]; then
|
|
export PROJECT_ID="mk2-prod"
|
|
echo "Switched PROJECT_ID to mk2-prod"
|
|
else
|
|
export PROJECT_ID="mk2-test"
|
|
echo "Switched PROJECT_ID to mk2-test"
|
|
fi
|
|
elif [[ "$1" == "test" ]]; then
|
|
export PROJECT_ID="mk2-test"
|
|
echo "Set PROJECT_ID to mk2-test"
|
|
elif [[ "$1" == "prod" ]]; then
|
|
export PROJECT_ID="mk2-prod"
|
|
echo "Set PROJECT_ID to mk2-prod"
|
|
else
|
|
echo "Usage: hr switch [test|prod]"
|
|
return 1
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
# Run original logic in a subshell to preserve set -e behavior without affecting current shell
|
|
(
|
|
set -e
|
|
# Restore arguments for processing
|
|
set -- "$command" "$@"
|
|
|
|
if [ "$1" = "init" ] && [ "$2" = "py" ]; then
|
|
_hr_init_py
|
|
elif [ "$1" = "init" ] && [ "$2" = "rs" ]; then
|
|
_hr_init_rs
|
|
elif [ "$1" = "init" ] && [ "$2" = "cpp" ]; then
|
|
_hr_init_cpp
|
|
elif [ "$1" = "freeze" ]; then
|
|
_hr_freeze
|
|
elif [ "$1" = "call" ]; then
|
|
shift
|
|
_hr_call "$@"
|
|
elif [ "$1" = "cf" ]; then
|
|
shift
|
|
_hr_cf "$@"
|
|
else
|
|
_hr_usage
|
|
exit 1
|
|
fi
|
|
)
|
|
}
|