| #!/usr/bin/env bash |
| # SPDX-License-Identifier: GPL-2.0 |
| |
| # Common Script Library for kernel test tools |
| |
| # --- Include Guard --- |
| # Prevents the library from being sourced multiple times. |
| |
| if [[ -n "${__COMMON_LIB_SOURCED__:-}" ]]; then |
| return 0 |
| fi |
| readonly __COMMON_LIB_SOURCED__=1 |
| |
| # --- Constants --- |
| readonly FETCH_SCRIPT_PATH_IN_REPO="kernel/tests/tools/fetch_artifact.sh" |
| readonly KERNEL_JDK_PATH="prebuilts/jdk/jdk11/linux-x86" |
| readonly LOCAL_JDK_PATH="/usr/local/buildtools/java/jdk11" |
| readonly PLATFORM_JDK_PATH="prebuilts/jdk/jdk21/linux-x86" |
| readonly DEFAULT_BUILD_CHECKER=/google/data/ro/projects/android/ab |
| |
| # --- BinFS --- |
| readonly COMMON_LIB_CL_FLASH_CLI="/google/bin/releases/android/flashstation/cl_flashstation" |
| readonly COMMON_LIB_LOCAL_FLASH_CLI="/google/bin/releases/android/flashstation/local_flashstation" |
| |
| # --- Download Path --- |
| if [[ -d "/tmp" ]]; then |
| readonly DOWNLOAD_PATH="/tmp/kernel_tests_downloads" |
| elif [[ -d "$HOME/Downloads" ]]; then |
| readonly DOWNLOAD_PATH="$HOME/Downloads/kernel_tests_downloads" |
| else |
| readonly DOWNLOAD_PATH="$PWD/out/kernel_tests_downloads" |
| fi |
| |
| # --- Device artifacts ---- |
| readonly DEVICE_DIR="$DOWNLOAD_PATH/device_dir" |
| readonly VENDOR_KERNEL_DIR="$DOWNLOAD_PATH/vendor_kernel_dir" |
| readonly KERNEL_DIR="$DOWNLOAD_PATH/kernel_dir" |
| readonly GSI_DIR="$DOWNLOAD_PATH/gsi_dir" |
| readonly TRADEFED_DIR="$DOWNLOAD_PATH/tradefed_dir" |
| |
| # --- Internal State Flags --- |
| __COMMON_LIB_NO_TPUT__="" # Flag set if tput is unavailable |
| |
| # --- Dependency Checks --- |
| if ! command -v tput &> /dev/null; then |
| echo "[WARN] common_lib.sh: Command 'tput' not found. Colored output disabled." >&2 |
| __COMMON_LIB_NO_TPUT__=1 |
| fi |
| |
| if ! command -v repo &> /dev/null; then |
| echo "[ERROR] common_lib.sh: Required command 'repo' not found. This library needs 'repo'." >&2 |
| return 1 |
| fi |
| |
| if ! command -v date &> /dev/null; then |
| echo "[ERROR] common_lib.sh: Required command 'date' not found. Timestamping will fail." >&2 |
| return 1 |
| fi |
| |
| # --- Color Constants --- |
| # Initialize empty, setup if tput exists and stdout is a terminal. |
| BLUE="" |
| BOLD="" |
| END="" |
| GREEN="" |
| ORANGE="" |
| RED="" |
| YELLOW="" |
| if [[ -z "$__COMMON_LIB_NO_TPUT__" && -t 1 ]]; then |
| BLUE=$(tput setaf 4 2>/dev/null) |
| BOLD=$(tput bold 2>/dev/null) |
| END=$(tput sgr0 2>/dev/null) |
| GREEN=$(tput setaf 2 2>/dev/null) |
| ORANGE=$(tput setaf 208 2>/dev/null || tput setaf 3 2>/dev/null) # Fallback orange |
| RED=$(tput setaf 198 2>/dev/null || tput setaf 1 2>/dev/null) # Fallback red |
| YELLOW=$(tput setaf 3 2>/dev/null) |
| |
| # Basic check if tput commands worked (might fail on minimal terminals) |
| if [[ -z "$BLUE" || -z "$BOLD" || -z "$END" ]]; then |
| echo "[WARN] common_lib.sh: tput commands failed to set colors properly. Disabling colors." >&2 |
| BLUE="" BOLD="" END="" GREEN="" ORANGE="" RED="" YELLOW="" |
| fi |
| fi |
| # Make color variables readonly after setting |
| readonly BLUE BOLD END GREEN ORANGE RED YELLOW |
| |
| # --- Internal Helper --- |
| |
| function _timestamp() { |
| local ts="" |
| ts=$(date +"%Y-%m-%d %H:%M:%S") |
| if ts=$(date +"%Y-%m-%d %H:%M:%S" 2>/dev/null); then |
| printf "%s" "$ts" |
| return 0 |
| else |
| # If date command fails entirely |
| printf "TIMESTAMP_ERR" >&2 # Avoid calling log_error to prevent recursion |
| return 1 |
| fi |
| } |
| |
| function _print_log() { |
| local log_level="$1" |
| local color_code="$2" |
| local message="$3" |
| local exit_code="${4:-}" # Optional exit code for context |
| local external_frame_hint="${5:-}" |
| local timestamp |
| timestamp=$(_timestamp) || timestamp="TIMESTAMP_ERR" |
| |
| local frame_to_report # This will hold the final frame number for 'caller' |
| if [[ -n "$external_frame_hint" && "$external_frame_hint" =~ ^[0-9]+$ ]]; then |
| # If an explicit frame hint is provided and is a number, use it directly. |
| frame_to_report="$external_frame_hint" |
| else |
| frame_to_report=1 |
| if [[ -n "$external_frame_hint" && ! "$external_frame_hint" =~ ^[0-9]+$ ]]; then |
| echo "[WARN] common_lib.sh: Invalid external_frame_hint '$external_frame_hint' provided to _print_log. Using default frame 1." >&2 |
| fi |
| fi |
| |
| # Get caller info (line, function, script) - 'caller 1' gets info about the caller of log_info/warn/error |
| local caller_info |
| caller_info=$(caller "$frame_to_report" 2>/dev/null) || caller_info="? <unknown> <unknown>" |
| |
| # Simple parsing of caller output (e.g., "123 my_func ./script.sh") |
| local caller_line="" caller_function="" caller_path="" caller_file="" context_info="" |
| if read -r caller_line caller_function caller_path <<< "$caller_info"; then |
| caller_file=$(basename "$caller_path") |
| context_info="$caller_file:$caller_line ($caller_function)" |
| else |
| context_info="${caller_info}" # Fallback if parsing fails |
| fi |
| |
| # Format: TIMESTAMP LEVEL [script:line (function)]: Message |
| local log_prefix="${timestamp} ${log_level}" |
| local full_message_prefix="[${log_prefix} ${context_info}]: " |
| local full_message_suffix="" |
| |
| # Append exit code context for errors if provided and non-zero |
| if [[ "$log_level" == "ERROR" && -n "$exit_code" ]] && (( exit_code != 0 )); then |
| # Append color codes carefully around the exit code part |
| full_message_suffix=" (${BOLD}Exit Code ${exit_code}${END}${color_code})${END}" |
| fi |
| |
| # Determine output stream (stderr for WARN/ERROR) |
| local output_stream="/dev/stderr" |
| if [[ "$log_level" == "INFO" ]]; then |
| output_stream="/dev/stdout" |
| fi |
| |
| # Print using printf with %s for the message to handle special characters safely |
| # Structure: ColorStart Prefix Message Suffix ColorEnd Newline |
| printf "%s%s%s%s%s\n" "${full_message_prefix}" "${color_code}" "$message" "${full_message_suffix}" "${END}" > "$output_stream" |
| } |
| |
| # --- Public API Functions --- |
| |
| function log_info() { |
| _print_log "INFO" "${GREEN}" "$1" |
| } |
| |
| # Accepts an optional second argument for context (e.g., an exit code). |
| # Returns 0 (warnings don't indicate function failure). |
| function log_warn() { |
| local message="$1" |
| local exit_code="${2:-}" # Optional context code |
| _print_log "WARN" "${ORANGE}" "$message" "$exit_code" |
| return 0 |
| } |
| |
| |
| # Does NOT exit the script. |
| # The caller should check the return status of functions using log_error. |
| function log_error() { |
| local message="$1" |
| local exit_code="${2:-1}" # defaults to 1 |
| local caller_frame_offset="${3:-}" # Default to empty, _print_log handles the default logic |
| _print_log "ERROR" "${RED}" "$message" "$exit_code" "$caller_frame_offset" |
| # Indicate that an error occurred via return status, but don't exit. |
| if [[ "$exit_code" =~ ^[0-9]+$ ]]; then |
| return "$exit_code" |
| else |
| return 1 # Default failure code if provided one was non-numeric |
| fi |
| } |
| |
| function check_command() { |
| local cmd="$1" |
| if [[ -z "$cmd" ]]; then |
| log_error "Usage: check_command <cmd>" |
| return 1 |
| fi |
| |
| if command -v "$cmd" &> /dev/null; then |
| return 0 |
| else |
| return 1 |
| fi |
| } |
| |
| # Usage: check_commands_available "cmd1" "cmd2" ... |
| function check_commands_available() { |
| local -a commands_to_check=("$@") |
| if (( ${#commands_to_check[@]} == 0 )); then |
| log_warn "No commands provided to check_commands_available." |
| return 0 # Nothing to check |
| fi |
| |
| local all_available=true |
| local -a unavailable_commands=() |
| local cmd |
| |
| for cmd in "${commands_to_check[@]}"; do |
| if ! check_command "$cmd"; then |
| all_available=false |
| unavailable_commands+=("'$cmd'") |
| fi |
| done |
| |
| if "$all_available"; then |
| return 0 |
| else |
| local unavailable_list |
| unavailable_list=$(printf "%s, " "${unavailable_commands[@]}") |
| unavailable_list=${unavailable_list%, } # Remove trailing comma and space |
| log_error "The following required commands are not available: ${unavailable_list}" 1 |
| return 1 |
| fi |
| } |
| |
| function find_repo_root() { |
| local start_dir="${1:-$PWD}" |
| local current_dir |
| |
| # Resolve potential ~ and relative paths to absolute, physical path (-P) |
| # Use '--' to handle start_dir potentially starting with '-' |
| if ! current_dir=$(cd -- "$start_dir" &>/dev/null && pwd -P); then |
| log_error "Invalid or inaccessible starting directory: '$start_dir'" 1 |
| return 1 |
| fi |
| |
| # Search upwards for .repo directory, stopping at root '/' |
| while [[ "$current_dir" != "/" && ! -d "${current_dir}/.repo" ]]; do |
| current_dir=$(dirname -- "$current_dir") |
| done |
| |
| # Check if found (must have .repo and not be the filesystem root itself) |
| if [[ -d "${current_dir}/.repo" && "$current_dir" != "/" ]]; then |
| printf "%s\n" "$current_dir" # Print the found path to stdout |
| return 0 |
| fi |
| |
| log_warn "No .repo directory found in or above: '$start_dir'" 1 |
| return 1 |
| } |
| |
| function go_to_repo_root() { |
| local start_dir="${1:-$PWD}" |
| local repo_root |
| local cd_status |
| |
| log_info "Attempting to find repo root from '${start_dir}'" |
| |
| # Call find_repo_root, capture its output (the path) and exit status |
| # Use process substitution or command substitution carefully |
| if ! repo_root=$(find_repo_root "$start_dir"); then |
| log_error "Failed to find repo root directory. Cannot change directory." 1 |
| return 1 |
| fi |
| |
| if [[ -z "$repo_root" ]]; then |
| # Should not happen if find_repo_root returns 0, but good safety check |
| log_error "find_repo_root succeeded but returned an empty path. Cannot change directory." 1 |
| return 1 |
| fi |
| |
| if [[ $(pwd) == "$repo_root" ]]; then |
| log_info "The current directory is already the repo root: $PWD" |
| else |
| # Only log and change directory if we are not already in the repo root |
| log_info "Repo root found: '${repo_root}'. Changing directory..." |
| |
| cd -- "$repo_root" &>/dev/null |
| cd_status=$? |
| if (( cd_status != 0 )); then |
| log_error "Failed to change directory to: '${repo_root}'" "$cd_status" |
| return "$cd_status" |
| fi |
| log_info "Successfully changed directory to repo root: $PWD" |
| return 0 |
| fi |
| } |
| |
| function is_in_repo_workspace() { |
| local check_path="${1:-$PWD}" |
| local resolved_path |
| |
| if ! resolved_path=$(cd -- "$check_path" &>/dev/null && pwd -P); then |
| log_error "Invalid or inaccessible directory for repo check: '$check_path'" 1 |
| return 1 |
| fi |
| |
| # Run 'repo list' in a subshell to avoid affecting the main script's directory |
| # and to capture stderr in case of repo tool issues. Redirect stdout to /dev/null. |
| local repo_output repo_status |
| repo_output=$( (cd -- "$resolved_path" && repo list) 2>&1 >/dev/null ) |
| repo_status=$? |
| |
| if (( repo_status != 0 )); then |
| # Log detailed warning including repo command output for debugging |
| log_warn "'repo list' command failed (exit code $repo_status) in '$resolved_path'. Not a repo workspace or repo tool issue? Output: ${repo_output}" "$repo_status" |
| fi |
| return $repo_status |
| } |
| |
| function is_repo_root_dir() { |
| local root_path="$1" |
| local resolved_path |
| |
| if [[ -z "$root_path" ]]; then |
| log_error "Usage: is_repo_root_dir <path>" 1 |
| return 1 |
| fi |
| |
| # Resolve path robustly first |
| if ! resolved_path=$(cd -- "$root_path" &>/dev/null && pwd -P); then |
| # Log as warning because non-existence isn't strictly an error in logic, just a state. |
| log_warn "Directory does not exist or is inaccessible: '$root_path'" 1 |
| return 1 |
| fi |
| |
| if [[ ! -d "${resolved_path}/.repo" ]]; then |
| log_warn "Directory exists but is missing '.repo' subdirectory: '${resolved_path}'" 1 |
| return 1 |
| fi |
| |
| if is_in_repo_workspace "$resolved_path"; then |
| # Both .repo exists and 'repo list' works |
| log_info "Confirmed valid repo root directory: '$resolved_path'" |
| return 0 |
| else |
| log_error "Directory '${resolved_path}' contains '.repo' but 'repo list' failed. May be an incomplete or corrupted checkout." 1 |
| return 1 |
| fi |
| } |
| |
| function is_platform_repo() { |
| local repo_path="$1" |
| local resolved_path |
| |
| if [[ -z "$repo_path" ]]; then |
| log_error "Usage: is_platform_repo <path>" 1 |
| return 1 |
| fi |
| |
| if ! is_repo_root_dir "$repo_path"; then |
| log_error "'$repo_path' is not a valid repo root directory." 1 |
| return 1 |
| fi |
| |
| resolved_path=$(cd -- "$repo_path" &>/dev/null && pwd -P) # Should succeed if is_repo_root_dir passed |
| local output repo_status |
| # Run in a subshell to cd safely and capture output/errors |
| output=$( (cd -- "$resolved_path" && repo list -p) 2>&1 ) |
| repo_status=$? |
| |
| if (( repo_status != 0 )); then |
| log_error "'repo list -p' failed in '${resolved_path}' (Exit Code $repo_status):$(printf '\n%s' "$output")" "$repo_status" |
| return 1 |
| fi |
| |
| # --- Heuristic Check --- |
| # This check assumes common Android platform structure. |
| # It might need adjustment if the platform layout changes significantly. |
| log_info "Applying heuristic check based on 'repo list -p' output..." |
| if [[ "$output" != *"build/make"* && "$output" != *"build/soong"* ]]; then |
| log_warn "Directory '${resolved_path}' may not be an Android Platform repository (heuristic check failed: missing 'build/make' or 'build/soong' in 'repo list -p' output)." 1 |
| return 1 |
| fi |
| # --- End Heuristic Check --- |
| |
| log_info "Confirmed Android Platform repository (based on heuristic): ${resolved_path}" |
| return 0 |
| } |
| |
| function set_platform_repo() { |
| local product="$1" |
| local device_variant="$2" # e.g., "userdebug" |
| local platform_root="$3" |
| local lunch_target="${4:-}" # defaults to empty |
| local resolved_root |
| local envsetup_script |
| |
| # Validate arguments |
| if [[ -z "$product" || -z "$device_variant" || -z "$platform_root" ]]; then |
| log_error "Usage: set_platform_repo <product> <variant> <platform_root>" 1 |
| return 1 |
| fi |
| |
| # Validate platform_root is a usable platform repo directory |
| # This also resolves the path internally via is_repo_root_dir |
| if ! is_platform_repo "$platform_root"; then |
| # is_platform_repo already logs details |
| log_error "Validation failed for platform root: '$platform_root'" 1 |
| return 1 |
| fi |
| |
| # Get the resolved absolute path (already validated) |
| resolved_root=$(cd -- "$platform_root" && pwd -P) |
| |
| # Check for envsetup.sh existence robustly |
| envsetup_script="${resolved_root}/build/envsetup.sh" |
| if [[ ! -f "$envsetup_script" ]]; then |
| log_error "Cannot find build/envsetup.sh in specified platform root: '${resolved_root}'" 1 |
| return 1 |
| fi |
| |
| if [[ -z "$lunch_target" ]]; then |
| if [[ -f "${resolved_root}/build/release/release_configs/trunk_staging.textproto" ]]; then |
| lunch_target="${product}-trunk_staging-${device_variant}" |
| else |
| lunch_target="${product}-${device_variant}" |
| fi |
| log_info "Determined lunch target: ${BOLD}${lunch_target}${END}" |
| fi |
| |
| if [[ "$PWD" != "$resolved_root" ]]; then |
| # Temporarily change to the repo root to run the commands |
| # Use pushd/popd to manage directory changes reliably |
| log_info "Changing directory to '${resolved_root}' for setup..." |
| |
| pushd "$resolved_root" &> /dev/null || { log_error "Failed to pushd into platform root: '${resolved_root}'"; return 1; } |
| |
| log_info "Changed directory to '${resolved_root}' successfully." |
| fi |
| |
| # Source the setup script. This executes it in the CURRENT shell. |
| env_cmd=("." "${envsetup_script}") |
| log_info "Sourcing Script: ${env_cmd[*]}..." |
| run_command "${env_cmd[@]}" |
| local source_status=$? |
| if (( source_status != 0 )); then |
| log_error "Sourcing ${envsetup_script} failed." "$source_status" |
| popd > /dev/null || { log_error "'popd' failed after sourcing."; return 1; } |
| return "$source_status" |
| fi |
| |
| log_info "Sourced envsetup.sh successfully." |
| |
| # Run the lunch command (should be defined after sourcing envsetup.sh). |
| if ! check_command "lunch"; then |
| log_error "'lunch' command not found after sourcing envsetup.sh. Setup failed." 1 |
| popd > /dev/null || { log_error "'popd' failed after checking command."; return 1; } |
| return 1 |
| fi |
| |
| log_info "Running: ${BOLD}lunch ${lunch_target}${END}" |
| |
| local lunch_output |
| local temp_file |
| temp_file=$(mktemp) |
| lunch "${lunch_target}" 1>"$temp_file" 2>&1 |
| local lunch_status=$? |
| lunch_output=$(cat "$temp_file") |
| # Clean up the temporary file |
| rm "$temp_file" |
| |
| if [[ "$lunch_output" != *"error:"* ]]; then |
| log_info "Build environment successfully set for ${lunch_target}." |
| else |
| log_error "'lunch ${lunch_target}' failed. Output:$(printf '\n%s' "$lunch_output")" "$lunch_status" |
| popd > /dev/null || { log_error "'popd' failed after lunching target."; return 1; } |
| return "$lunch_status" |
| fi |
| |
| popd > /dev/null || log_warn "'popd' failed after successful setup. Current directory: $PWD" |
| |
| log_info "Setup complete. Returned to original directory via popd." |
| return 0 |
| } |
| |
| # Function to create softlink |
| function create_soft_link() { |
| local original_file_name="$1" |
| local soft_link_name="$2" |
| ln -s "$original_file_name" "$soft_link_name" |
| exit_code=$? |
| if (( exit_code == 0 )); then |
| log_info "Linked $original_file_name to $soft_link_name" |
| else |
| log_error "Failed to link $original_file_name to $soft_link_name" |
| return 1 |
| fi |
| } |
| |
| # Function to convert/normalize an 'ab://' string |
| function convert_ab_string() { |
| local original_ab_string="$1" |
| local __result_var="$2" # The name of the variable to hold the output |
| local ab_string_format="ab://<branch>/<build_target>/<build_id> or ab://<branch>/<build_target>/<build_id>/<file_name>" |
| local ab_string_error_message="Invalid build string: '$original_ab_string'. Needs to be: $ab_string_format" |
| |
| if [[ "$original_ab_string" != ab://* ]]; then |
| log_error "$ab_string_error_message" |
| return 1 |
| fi |
| |
| local path_string="${original_ab_string#ab://}" |
| IFS='/' read -ra array <<< "$path_string" |
| # The expected array length is 3 (branch/target/id) or 4 (branch/target/id/file) |
| local array_len="${#array[@]}" |
| if (( array_len < 3 || array_len > 4 )); then |
| log_error "$ab_string_error_message" |
| return 1 |
| fi |
| |
| local branch="${array[0]}" |
| local build_target="${array[1]}" |
| local build_id="" |
| local rest_string="" |
| if (( array_len == 3 )); then |
| build_id="${array[2]}" |
| if [[ -z "$build_id" ]]; then |
| build_id="latest" |
| fi |
| elif (( array_len == 4 )); then |
| build_id="${array[2]}" |
| rest_string="/${array[3]}" |
| fi |
| |
| if [[ "$build_id" == "latest" || "$build_id" == "lkgb" ]]; then |
| if [[ -f "$DEFAULT_BUILD_CHECKER" ]]; then |
| local output |
| local check_build_cmd="$DEFAULT_BUILD_CHECKER lkgb --branch $branch --target $build_target" |
| output=$("$DEFAULT_BUILD_CHECKER" lkgb --branch "$branch" --target "$build_target") |
| if [[ "$output" != *"$branch"* ]]; then |
| log_error "Command $DEFAULT_BUILD_CHECKER lkgb --branch $branch --target $build_target \ |
| returned: '$output'. Build target $branch/$build_target doesn't exist in go/ab" |
| return 1 |
| fi |
| build_id=$(echo "$output" | awk '{print $3}') |
| fi |
| fi |
| |
| local final_string="ab://$branch/$build_target/$build_id$rest_string" |
| if [[ "$original_ab_string" != "$final_string" ]]; then |
| log_info "Input: $original_ab_string -> Converted: $final_string" |
| fi |
| printf -v "$__result_var" "%s" "$final_string" |
| return 0 |
| } |
| |
| function parse_ab_url() { |
| local url="$1" |
| local branch_var="$2" |
| local target_var="$3" |
| local id_var="$4" |
| |
| if [[ "$url" != ab://* ]]; then |
| log_error "Invalid ab URL format: $url" 1 |
| return 1 |
| fi |
| |
| local path_part="${url#ab://}" |
| local -a parts=() |
| local IFS='/' |
| read -r -a parts <<< "$path_part" |
| |
| if (( ${#parts[@]} < 2 )); then # Must have at least branch and target |
| log_error "Malformed ab URL (not enough parts): $url" 1 |
| return 1 |
| fi |
| |
| if [[ -z "${parts[0]}" ]]; then |
| log_error "branch variable has no value, check url format: $url" 1 |
| return 1 |
| fi |
| |
| if [[ -z "${parts[1]}" ]]; then |
| log_error "target variable has no value, check url format: $url" 1 |
| return 1 |
| fi |
| printf -v "$branch_var" "%s" "${parts[0]}" |
| printf -v "$target_var" "%s" "${parts[1]}" |
| |
| if (( ${#parts[@]} >= 3 )) && [[ -n "${parts[2]}" ]]; then |
| printf -v "$id_var" "%s" "${parts[2]}" |
| else |
| log_warn "id variable is empty, use 'latest' as default id" |
| printf -v "$id_var" "%s" "latest" |
| fi |
| return 0 |
| } |
| |
| function run_command() { |
| local -a command_to_run=("$@") |
| local status_code |
| |
| # log_info "Running: '${command_to_run[*]}'" |
| |
| "${command_to_run[@]}" |
| status_code=$? |
| if (( status_code == 0 )); then |
| log_info "Succeeded." |
| else |
| log_error "Failed." "$status_code" |
| fi |
| |
| return $status_code |
| } |
| |
| function set_env_var() { |
| local var_name="$1" |
| local var_value="$2" |
| |
| # Validate variable name (POSIX-compliant) |
| if ! [[ "$var_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then |
| log_error "Invalid environment variable name '$var_name'" |
| return 1 |
| fi |
| |
| export "$var_name=$var_value" |
| log_info "Exported environment variable: ${var_name}='${var_value}'" |
| return 0 |
| } |