如何使用环境模块文件(tcl脚本)加载虚拟环境?

12 投票
4 回答
4825 浏览
提问于 2025-04-18 01:30

我正在尝试为一个程序写一个模块文件,这个程序需要创建一个 Python 的 virtualenv(虚拟环境)。为了启动这个 virtualenv,它需要先运行 /programs/program-env/bin/activate。请问我该如何在 modulefile 中做到这一点?

任何帮助都将非常感谢。

注意:我试着把上面的那行直接放进文件里,但没有成功。

谢谢,

编辑:

我正在写一个 modulefile,用来加载一个只能在 virtualenv 中运行的程序。通常这些模块文件会设置一些变量名或者把 bin 目录添加到路径中。由于这个包有点不同,我不知道该怎么继续。你可以在 这里找到一个示例模块文件。

4 个回答

0

你没有很清楚地说明你想做什么,但因为你在标题中提到了tcl脚本,我假设你是在写一个需要加载虚拟环境的Tcl脚本,以便使用虚拟环境的配置来操作Python脚本。激活脚本是bash脚本,主要是用来设置当前的环境。你不能直接把这些脚本引入到Tcl中,因为Tcl并不是Bourne shell。不过,你可以创建一个shell子进程,读取它的环境,然后把这个环境和激活脚本引入后的环境进行比较。如果你的Tcl脚本把这些差异应用到它自己的环境中,那么最终的Tcl进程就相当于在引入激活脚本后的bash shell。

这里有个例子。如果你运行这个命令 tclsh scriptname bin/activate,它会打印出环境变量,这时会包含激活脚本中的额外设置。在我测试的Linux系统上,这会增加一个VIRTUAL_ENV变量,并修改PS1和PATH。

#!/usr/bin/env tclsh
# Load a virtualenv script in a subshell and apply the environment
# changes to the current process environment.

proc read_env {chan varname} {
    upvar #0 $varname E
    set len [gets $chan line]
    if {$len < 0} {
        fileevent $chan readable {}
        set ::completed 1
    } else {
        set pos [string first = $line]
        set key [string range $line 0 [expr {$pos - 1}]]
        set val [string range $line [expr {$pos + 1}] end]
        set E($key) $val
    }
}

proc read_shell_env {varname cmd} {
    set shell [open |[list /bin/bash] "r+"]
    fconfigure $shell -buffering line -encoding utf-8 -blocking 0
    fileevent $shell readable [list read_env $shell $varname]
    puts $shell $cmd
    flush $shell
    vwait ::completed
    close $shell
    return
}

proc update_env {key val} {
    global env
    set env($key) $val
}

proc load_virtualenv {filename} {
    array set ::envA {}
    array set ::envB {}
    read_shell_env ::envA "printenv; exit 0"
    read_shell_env ::envB "source \"$filename\"; printenv; exit 0"

    set keys [lsort [array names ::envA]]
    foreach k [lsort [array names ::envB]] {
        if {[info exists ::envA($k)]} {
            if {$::envA($k) ne $::envB($k)} {
                update_env $k $::envB($k)
            }
        } else {
            update_env $k $::envB($k)
        }
    }
    unset ::envA
    unset ::envB
    return
}

proc main {filename} {
    global env
    load_virtualenv $filename
    foreach key [lsort [array names env]] {
        puts "$key=$env($key)"
    }
    return 0
}

if {!$tcl_interactive} {
    set r [catch [linsert $argv 0 main] err]
    if {$r} {puts stderr $err}
    exit $r
}
5

根据Donal Fellows的回答和相关文档,可以这样做:

if { [ module-info mode load ] } {
    puts stdout "/programs/program-env/bin/activate;"
} elseif { [ module-info mode remove ] } {
    puts stdout "deactivate;"
}

分号是必不可少的。

6

模块系统有点奇怪,因为它实际上是在创建一组指令,这些指令是由调用它的命令行来执行的。这意味着,普通的Tcl做事情的方法往往不太适用;真正需要运行的其实是调用者,而不是Tcl脚本本身,调用者需要执行 /programs/program-env/bin/activate

首先可以尝试的是:

system "/programs/program-env/bin/activate"

不过,在常见问题解答中,我发现你可能需要这样做(加上保护措施):

if {[module-info mode] == "load"} {
    puts stdout "/programs/program-env/bin/activate"
}

我不知道如何反向操作(这也是模块的一个目的)。

13

这里有一个稍微详细一点的回答,基于Donal和betapatch的回答,帮助你在两个功能相似的模块之间切换:

if { [module-info mode load] || [module-info mode switch2] } {
    puts stdout "source /programs/program-env/bin/activate;"
} elseif { [module-info mode remove] && ![module-info mode switch3] } {
    puts stdout "deactivate;"
}

首先,你需要使用 source .../activate,而不是直接用 .../activate

其次,modules 在切换模块时有些复杂。如果你想用 module swap foo bar(也就是移除 foo,然后加载 bar),实际上它会执行以下操作:

foo: switch1 # prep for remove
foo: remove  # actually remove
bar: switch2 # load new module
foo: switch3 # cleanup
foo: remove  # happens at the same time as foo switch3

这意味着如果 foobar 都是使用虚拟环境的模块文件,第二个 foo remove 会导致 bardeactivate(停用)。

撰写回答