From a80a0a82110fab8972b0e8e796c2d6dad1147a63 Mon Sep 17 00:00:00 2001 From: Jocadbz Date: Mon, 29 Dec 2025 17:13:48 -0300 Subject: [PATCH] Implement temporary development environment (devenv) with shell detection and activation scripts --- config/config.v | 6 + devenv/devenv.v | 386 ++++++++++++++++++++++++++++++++++++++++++++ docs/help.txt | 10 ++ lana.v | 33 ++++ tests/devenv_test.v | 182 +++++++++++++++++++++ 5 files changed, 617 insertions(+) create mode 100644 devenv/devenv.v create mode 100644 tests/devenv_test.v diff --git a/config/config.v b/config/config.v index ad78505..7f12459 100644 --- a/config/config.v +++ b/config/config.v @@ -89,6 +89,9 @@ pub mut: // Build directives from source files build_directives []BuildDirective + + // Devenv configuration for temporary development environment + devenv_lib_paths []string // optional separate lib paths for devenv (falls back to lib_search_paths) } struct RawGlobalConfig { @@ -110,6 +113,7 @@ mut: cflags []string ldflags []string dependencies_dir string + devenv_lib_paths []string } struct RawSharedLibConfig { @@ -271,6 +275,7 @@ fn normalize_raw_config(raw RawBuildConfig, mut warnings []string) BuildConfig { cfg.libraries = inherit_list(raw.global.libraries, cfg.libraries) cfg.cflags = inherit_list(raw.global.cflags, cfg.cflags) cfg.ldflags = inherit_list(raw.global.ldflags, cfg.ldflags) + cfg.devenv_lib_paths = inherit_list(raw.global.devenv_lib_paths, cfg.devenv_lib_paths) for raw_lib in raw.shared_libs { scope := scope_label(raw_lib.name, 'shared_lib') @@ -671,6 +676,7 @@ pub fn parse_config_file(filename string) !BuildConfig { 'parallel_compilation' { raw.global.parallel_str = value } 'include_dirs' { raw.global.include_dirs << parse_comma_list(value) } 'lib_search_paths' { raw.global.lib_search_paths << parse_comma_list(value) } + 'devenv_lib_paths' { raw.global.devenv_lib_paths << parse_comma_list(value) } 'libraries' { raw.global.libraries << parse_comma_list(value) } 'cflags' { raw.global.cflags << parse_space_list(value) } 'ldflags' { raw.global.ldflags << parse_space_list(value) } diff --git a/devenv/devenv.v b/devenv/devenv.v new file mode 100644 index 0000000..f606888 --- /dev/null +++ b/devenv/devenv.v @@ -0,0 +1,386 @@ +module devenv + +import os +import config + +// ShellType represents the detected shell type +pub enum ShellType { + bash + zsh + fish + sh + unknown +} + +// DevEnvConfig holds configuration for the temporary dev environment +pub struct DevEnvConfig { +pub: + lib_search_paths []string + project_name string + bin_dir string +} + +// detect_shell detects the current running shell from environment variables +pub fn detect_shell() ShellType { + // Try SHELL environment variable first (user's default shell) + shell_env := os.getenv('SHELL') + if shell_env != '' { + return parse_shell_name(shell_env) + } + + // Fallback: try to get parent process shell (more accurate for current shell) + // This is a best-effort approach + return ShellType.unknown +} + +// parse_shell_name extracts shell type from a shell path or name +pub fn parse_shell_name(shell_path string) ShellType { + // Get the basename of the shell path + mut shell_name := shell_path + if shell_path.contains('/') { + parts := shell_path.split('/') + if parts.len > 0 { + shell_name = parts[parts.len - 1] + } + } + + return match shell_name { + 'bash' { ShellType.bash } + 'zsh' { ShellType.zsh } + 'fish' { ShellType.fish } + 'sh' { ShellType.sh } + else { ShellType.unknown } + } +} + +// shell_type_to_string converts ShellType to a human-readable string +pub fn shell_type_to_string(shell ShellType) string { + return match shell { + .bash { 'bash' } + .zsh { 'zsh' } + .fish { 'fish' } + .sh { 'sh' } + .unknown { 'unknown' } + } +} + +// get_devenv_config extracts dev environment config from BuildConfig +pub fn get_devenv_config(build_config config.BuildConfig) DevEnvConfig { + // Use devenv-specific paths if configured, otherwise fall back to lib_search_paths + mut lib_paths := []string{} + + // Priority: devenv_lib_paths > lib_search_paths + if build_config.devenv_lib_paths.len > 0 { + lib_paths = build_config.devenv_lib_paths.clone() + } else { + lib_paths = build_config.lib_search_paths.clone() + } + + // Add default library paths if not already present + default_lib_path := os.join_path(build_config.bin_dir, 'lib') + if default_lib_path !in lib_paths { + lib_paths << default_lib_path + } + + return DevEnvConfig{ + lib_search_paths: lib_paths + project_name: build_config.project_name + bin_dir: build_config.bin_dir + } +} + +// generate_activation_script generates shell-specific activation script content +pub fn generate_activation_script(shell ShellType, devenv_config DevEnvConfig) !string { + // Get absolute paths for lib_search_paths + cwd := os.getwd() + mut absolute_paths := []string{} + for path in devenv_config.lib_search_paths { + if os.is_abs_path(path) { + absolute_paths << path + } else { + absolute_paths << os.join_path(cwd, path) + } + } + + paths_str := absolute_paths.join(':') + project_name := if devenv_config.project_name != '' { + devenv_config.project_name + } else { + 'lana-project' + } + + return match shell { + .bash { generate_bash_script(paths_str, project_name) } + .zsh { generate_zsh_script(paths_str, project_name) } + .fish { generate_fish_script(paths_str, project_name) } + .sh { generate_sh_script(paths_str, project_name) } + .unknown { error('Cannot generate activation script for unknown shell. Please specify your shell manually.') } + } +} + +// generate_bash_script creates a bash activation script +fn generate_bash_script(lib_paths string, project_name string) string { + return '# Lana temporary development environment activation script (bash) +# Source this file to activate the environment: source <(lana devenv) + +# Save original values for deactivation +_LANA_OLD_PS1="\${PS1:-}" +_LANA_OLD_LD_LIBRARY_PATH="\${LD_LIBRARY_PATH:-}" +_LANA_ACTIVE=1 + +# Update LD_LIBRARY_PATH to include project library paths +if [ -z "\$LD_LIBRARY_PATH" ]; then + export LD_LIBRARY_PATH="${lib_paths}" +else + export LD_LIBRARY_PATH="${lib_paths}:\$LD_LIBRARY_PATH" +fi + +# Modify prompt to show we\'re in a Lana dev environment +export PS1="(Lana\'s temp environment: ${project_name}) \${PS1}" + +# Define deactivate function +deactivate() { + if [ -n "\$_LANA_ACTIVE" ]; then + # Restore original LD_LIBRARY_PATH + if [ -n "\$_LANA_OLD_LD_LIBRARY_PATH" ]; then + export LD_LIBRARY_PATH="\$_LANA_OLD_LD_LIBRARY_PATH" + else + unset LD_LIBRARY_PATH + fi + + # Restore original PS1 + export PS1="\$_LANA_OLD_PS1" + + # Clean up + unset _LANA_OLD_PS1 + unset _LANA_OLD_LD_LIBRARY_PATH + unset _LANA_ACTIVE + unset -f deactivate + + echo "Lana development environment deactivated." + fi +} + +echo "Lana development environment activated for ${project_name}." +echo "Library search paths: ${lib_paths}" +echo "Run \'deactivate\' to exit the environment." +' +} + +// generate_zsh_script creates a zsh activation script +fn generate_zsh_script(lib_paths string, project_name string) string { + return '# Lana temporary development environment activation script (zsh) +# Source this file to activate the environment: source <(lana devenv) + +# Save original values for deactivation +_LANA_OLD_PS1="\${PS1:-}" +_LANA_OLD_LD_LIBRARY_PATH="\${LD_LIBRARY_PATH:-}" +_LANA_ACTIVE=1 + +# Update LD_LIBRARY_PATH to include project library paths +if [ -z "\$LD_LIBRARY_PATH" ]; then + export LD_LIBRARY_PATH="${lib_paths}" +else + export LD_LIBRARY_PATH="${lib_paths}:\$LD_LIBRARY_PATH" +fi + +# Modify prompt to show we\'re in a Lana dev environment +export PS1="(Lana\'s temp environment: ${project_name}) \${PS1}" + +# Define deactivate function +deactivate() { + if [ -n "\$_LANA_ACTIVE" ]; then + # Restore original LD_LIBRARY_PATH + if [ -n "\$_LANA_OLD_LD_LIBRARY_PATH" ]; then + export LD_LIBRARY_PATH="\$_LANA_OLD_LD_LIBRARY_PATH" + else + unset LD_LIBRARY_PATH + fi + + # Restore original PS1 + export PS1="\$_LANA_OLD_PS1" + + # Clean up + unset _LANA_OLD_PS1 + unset _LANA_OLD_LD_LIBRARY_PATH + unset _LANA_ACTIVE + unset -f deactivate + + echo "Lana development environment deactivated." + fi +} + +echo "Lana development environment activated for ${project_name}." +echo "Library search paths: ${lib_paths}" +echo "Run \'deactivate\' to exit the environment." +' +} + +// generate_fish_script creates a fish shell activation script +fn generate_fish_script(lib_paths string, project_name string) string { + return '# Lana temporary development environment activation script (fish) +# Source this file to activate the environment: source (lana devenv | psub) + +# Save original values for deactivation +if set -q LD_LIBRARY_PATH + set -gx _LANA_OLD_LD_LIBRARY_PATH ${"$"}LD_LIBRARY_PATH +else + set -gx _LANA_OLD_LD_LIBRARY_PATH "" +end + +set -gx _LANA_ACTIVE 1 + +# Update LD_LIBRARY_PATH to include project library paths +if test -z "${"$"}LD_LIBRARY_PATH" + set -gx LD_LIBRARY_PATH "${lib_paths}" +else + set -gx LD_LIBRARY_PATH "${lib_paths}:${"$"}LD_LIBRARY_PATH" +end + +# Store original fish_prompt function +functions -c fish_prompt _lana_old_fish_prompt + +# Override fish_prompt to show Lana environment +function fish_prompt + echo -n "(Lana\'s temp environment: ${project_name}) " + _lana_old_fish_prompt +end + +# Define deactivate function +function deactivate + if set -q _LANA_ACTIVE + # Restore original LD_LIBRARY_PATH + if test -n "${"$"}_LANA_OLD_LD_LIBRARY_PATH" + set -gx LD_LIBRARY_PATH ${"$"}_LANA_OLD_LD_LIBRARY_PATH + else + set -e LD_LIBRARY_PATH + end + + # Restore original fish_prompt + functions -e fish_prompt + functions -c _lana_old_fish_prompt fish_prompt + functions -e _lana_old_fish_prompt + + # Clean up + set -e _LANA_OLD_LD_LIBRARY_PATH + set -e _LANA_ACTIVE + functions -e deactivate + + echo "Lana development environment deactivated." + end +end + +echo "Lana development environment activated for ${project_name}." +echo "Library search paths: ${lib_paths}" +echo "Run \'deactivate\' to exit the environment." +' +} + +// generate_sh_script creates a POSIX sh activation script +fn generate_sh_script(lib_paths string, project_name string) string { + return '# Lana temporary development environment activation script (sh) +# Source this file to activate the environment: . $(lana devenv --output-file) + +# Save original values for deactivation +_LANA_OLD_PS1="${'$'}PS1" +_LANA_OLD_LD_LIBRARY_PATH="${'$'}LD_LIBRARY_PATH" +_LANA_ACTIVE=1 + +# Update LD_LIBRARY_PATH to include project library paths +if [ -z "${'$'}LD_LIBRARY_PATH" ]; then + LD_LIBRARY_PATH="${lib_paths}" +else + LD_LIBRARY_PATH="${lib_paths}:${'$'}LD_LIBRARY_PATH" +fi +export LD_LIBRARY_PATH + +# Modify prompt to show we\'re in a Lana dev environment +PS1="(Lana\'s temp environment: ${project_name}) ${'$'}PS1" +export PS1 + +# Define deactivate function +deactivate() { + if [ -n "${'$'}_LANA_ACTIVE" ]; then + # Restore original LD_LIBRARY_PATH + if [ -n "${'$'}_LANA_OLD_LD_LIBRARY_PATH" ]; then + LD_LIBRARY_PATH="${'$'}_LANA_OLD_LD_LIBRARY_PATH" + export LD_LIBRARY_PATH + else + unset LD_LIBRARY_PATH + fi + + # Restore original PS1 + PS1="${'$'}_LANA_OLD_PS1" + export PS1 + + # Clean up + unset _LANA_OLD_PS1 + unset _LANA_OLD_LD_LIBRARY_PATH + unset _LANA_ACTIVE + + echo "Lana development environment deactivated." + fi +} + +echo "Lana development environment activated for ${project_name}." +echo "Library search paths: ${lib_paths}" +echo "Run \'deactivate\' to exit the environment." +' +} + +// activate_devenv is the main entry point for the devenv command +pub fn activate_devenv(build_config config.BuildConfig, shell_override string) ! { + // Detect shell or use override + mut shell := detect_shell() + if shell_override != '' { + shell = parse_shell_name(shell_override) + if shell == .unknown { + return error('Unknown shell specified: ${shell_override}. Supported shells: bash, zsh, fish, sh') + } + } + + if shell == .unknown { + // Try to provide helpful message + eprintln('Could not detect your shell automatically.') + eprintln('Please specify your shell using: lana devenv --shell ') + eprintln('Or set the SHELL environment variable.') + return error('Shell detection failed') + } + + // Get dev environment configuration + devenv_config := get_devenv_config(build_config) + + // Generate and print the activation script + script := generate_activation_script(shell, devenv_config)! + print(script) +} + +// print_devenv_info prints information about the dev environment without activating +pub fn print_devenv_info(build_config config.BuildConfig) { + devenv_config := get_devenv_config(build_config) + shell := detect_shell() + + println('Lana Temporary Development Environment') + println('======================================') + println('') + println('Detected shell: ${shell_type_to_string(shell)}') + println('Project name: ${devenv_config.project_name}') + println('Library search paths:') + for path in devenv_config.lib_search_paths { + println(' - ${path}') + } + println('') + println('To activate the environment:') + + match shell { + .bash, .zsh { println(' source <(lana devenv)') } + .fish { println(' source (lana devenv | psub)') } + .sh { println(' eval "$(lana devenv)"') } + .unknown { + println(' # Shell not detected. Use --shell to specify:') + println(' source <(lana devenv --shell bash)') + } + } + println('') + println('To deactivate: run \'deactivate\' in your shell') +} diff --git a/docs/help.txt b/docs/help.txt index 451b298..c83d085 100644 --- a/docs/help.txt +++ b/docs/help.txt @@ -7,8 +7,18 @@ Commands: clean Remove build artifacts under build/ and bin/. init Scaffold a new Lana-ready C++ project. setup Fetch/build external dependencies declared in config.ini. + devenv Generate shell script to activate temporary development environment. help Display this help text. +Devenv Options: + --shell, -s Specify shell type (bash, zsh, fish, sh). + --info, -i Show environment info without generating script. + + Usage examples: + bash/zsh: source <(lana devenv) + fish: source (lana devenv | psub) + sh: eval "$(lana devenv)" + Global Options: -d, --debug Enable debug mode (-g -O0). -O, --optimize Enable optimization (-O3, disables debug). diff --git a/lana.v b/lana.v index 2b051a6..0d1b6dd 100644 --- a/lana.v +++ b/lana.v @@ -8,6 +8,7 @@ import initializer import deps import help import util +import devenv fn main() { mut config_data := config.parse_args() or { config.default_config } @@ -55,6 +56,38 @@ fn main() { exit(1) } } + 'devenv' { + // Parse devenv-specific options + mut shell_override := '' + mut show_info := false + + mut i := 2 + for i < os.args.len { + arg := os.args[i] + match arg { + '--shell', '-s' { + if i + 1 < os.args.len { + shell_override = os.args[i + 1] + i += 1 + } + } + '--info', '-i' { + show_info = true + } + else {} + } + i += 1 + } + + if show_info { + devenv.print_devenv_info(config_data) + } else { + devenv.activate_devenv(config_data, shell_override) or { + eprintln('Failed to generate devenv script: ${err}') + exit(1) + } + } + } else { help.show_help() } diff --git a/tests/devenv_test.v b/tests/devenv_test.v new file mode 100644 index 0000000..b18fb16 --- /dev/null +++ b/tests/devenv_test.v @@ -0,0 +1,182 @@ +module tests + +import os +import config +import devenv + +fn test_detect_shell_from_env() { + // Test shell detection parsing + assert devenv.parse_shell_name('/bin/bash') == devenv.ShellType.bash + assert devenv.parse_shell_name('/usr/bin/zsh') == devenv.ShellType.zsh + assert devenv.parse_shell_name('/usr/local/bin/fish') == devenv.ShellType.fish + assert devenv.parse_shell_name('/bin/sh') == devenv.ShellType.sh + assert devenv.parse_shell_name('bash') == devenv.ShellType.bash + assert devenv.parse_shell_name('zsh') == devenv.ShellType.zsh + assert devenv.parse_shell_name('fish') == devenv.ShellType.fish + assert devenv.parse_shell_name('sh') == devenv.ShellType.sh + assert devenv.parse_shell_name('unknown_shell') == devenv.ShellType.unknown +} + +fn test_shell_type_to_string() { + assert devenv.shell_type_to_string(devenv.ShellType.bash) == 'bash' + assert devenv.shell_type_to_string(devenv.ShellType.zsh) == 'zsh' + assert devenv.shell_type_to_string(devenv.ShellType.fish) == 'fish' + assert devenv.shell_type_to_string(devenv.ShellType.sh) == 'sh' + assert devenv.shell_type_to_string(devenv.ShellType.unknown) == 'unknown' +} + +fn test_get_devenv_config_uses_lib_search_paths_by_default() { + build_config := config.BuildConfig{ + project_name: 'testproject' + bin_dir: 'bin' + lib_search_paths: ['custom/lib', 'other/lib'] + devenv_lib_paths: [] + } + + devenv_config := devenv.get_devenv_config(build_config) + + assert devenv_config.project_name == 'testproject' + assert 'custom/lib' in devenv_config.lib_search_paths + assert 'other/lib' in devenv_config.lib_search_paths + assert 'bin/lib' in devenv_config.lib_search_paths +} + +fn test_get_devenv_config_uses_devenv_lib_paths_when_set() { + build_config := config.BuildConfig{ + project_name: 'testproject' + bin_dir: 'bin' + lib_search_paths: ['default/lib'] + devenv_lib_paths: ['devenv/lib', 'devenv/extra'] + } + + devenv_config := devenv.get_devenv_config(build_config) + + assert devenv_config.project_name == 'testproject' + // devenv_lib_paths should take priority + assert 'devenv/lib' in devenv_config.lib_search_paths + assert 'devenv/extra' in devenv_config.lib_search_paths + // default lib path should still be added + assert 'bin/lib' in devenv_config.lib_search_paths + // lib_search_paths should NOT be used when devenv_lib_paths is set + assert 'default/lib' !in devenv_config.lib_search_paths +} + +fn test_generate_bash_script_contains_deactivate() { + devenv_config := devenv.DevEnvConfig{ + lib_search_paths: ['/path/to/lib'] + project_name: 'myproject' + bin_dir: 'bin' + } + + script := devenv.generate_activation_script(devenv.ShellType.bash, devenv_config) or { + assert false, 'Failed to generate bash script: ${err}' + return + } + + assert script.contains('deactivate') + assert script.contains('LD_LIBRARY_PATH') + assert script.contains('myproject') + assert script.contains('/path/to/lib') + assert script.contains("Lana's temp environment") +} + +fn test_generate_fish_script_contains_deactivate() { + devenv_config := devenv.DevEnvConfig{ + lib_search_paths: ['/path/to/lib'] + project_name: 'myproject' + bin_dir: 'bin' + } + + script := devenv.generate_activation_script(devenv.ShellType.fish, devenv_config) or { + assert false, 'Failed to generate fish script: ${err}' + return + } + + assert script.contains('function deactivate') + assert script.contains('LD_LIBRARY_PATH') + assert script.contains('myproject') + assert script.contains('fish_prompt') +} + +fn test_generate_zsh_script_contains_deactivate() { + devenv_config := devenv.DevEnvConfig{ + lib_search_paths: ['/path/to/lib'] + project_name: 'myproject' + bin_dir: 'bin' + } + + script := devenv.generate_activation_script(devenv.ShellType.zsh, devenv_config) or { + assert false, 'Failed to generate zsh script: ${err}' + return + } + + assert script.contains('deactivate') + assert script.contains('LD_LIBRARY_PATH') + assert script.contains('PS1') +} + +fn test_generate_sh_script_contains_deactivate() { + devenv_config := devenv.DevEnvConfig{ + lib_search_paths: ['/path/to/lib'] + project_name: 'myproject' + bin_dir: 'bin' + } + + script := devenv.generate_activation_script(devenv.ShellType.sh, devenv_config) or { + assert false, 'Failed to generate sh script: ${err}' + return + } + + assert script.contains('deactivate') + assert script.contains('LD_LIBRARY_PATH') +} + +fn test_unknown_shell_returns_error() { + devenv_config := devenv.DevEnvConfig{ + lib_search_paths: ['/path/to/lib'] + project_name: 'myproject' + bin_dir: 'bin' + } + + script := devenv.generate_activation_script(devenv.ShellType.unknown, devenv_config) or { + // Expected to fail + assert err.msg().contains('unknown shell') + return + } + + assert false, 'Should have returned error for unknown shell' +} + +fn test_config_parsing_devenv_lib_paths() { + tmp := new_temp_dir('lana_devenv_config') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + config_content := '[global]\nproject_name = testproj\nlib_search_paths = default/lib\ndevenv_lib_paths = devenv/lib1, devenv/lib2\n' + os.write_file(config_path, config_content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + assert cfg.project_name == 'testproj' + assert cfg.lib_search_paths.contains('default/lib') + assert cfg.devenv_lib_paths.contains('devenv/lib1') + assert cfg.devenv_lib_paths.contains('devenv/lib2') +} + +fn test_multiple_lib_paths_joined_with_colon() { + devenv_config := devenv.DevEnvConfig{ + lib_search_paths: ['/path/one', '/path/two', '/path/three'] + project_name: 'myproject' + bin_dir: 'bin' + } + + script := devenv.generate_activation_script(devenv.ShellType.bash, devenv_config) or { + assert false, 'Failed to generate script: ${err}' + return + } + + // Paths should be joined with colons in the script + assert script.contains('/path/one:/path/two:/path/three') +}