如何使用环境模块文件(tcl脚本)加载虚拟环境?
我正在尝试为一个程序写一个模块文件,这个程序需要创建一个 Python 的 virtualenv
(虚拟环境)。为了启动这个 virtualenv
,它需要先运行 /programs/program-env/bin/activate
。请问我该如何在 modulefile
中做到这一点?
任何帮助都将非常感谢。
注意:我试着把上面的那行直接放进文件里,但没有成功。
谢谢,
编辑:
我正在写一个 modulefile,用来加载一个只能在 virtualenv
中运行的程序。通常这些模块文件会设置一些变量名或者把 bin 目录添加到路径中。由于这个包有点不同,我不知道该怎么继续。你可以在 这里找到一个示例模块文件。
4 个回答
你没有很清楚地说明你想做什么,但因为你在标题中提到了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
}
根据Donal Fellows的回答和相关文档,可以这样做:
if { [ module-info mode load ] } {
puts stdout "/programs/program-env/bin/activate;"
} elseif { [ module-info mode remove ] } {
puts stdout "deactivate;"
}
分号是必不可少的。
模块系统有点奇怪,因为它实际上是在创建一组指令,这些指令是由调用它的命令行来执行的。这意味着,普通的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"
}
我不知道如何反向操作(这也是模块的一个目的)。
这里有一个稍微详细一点的回答,基于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
这意味着如果 foo
和 bar
都是使用虚拟环境的模块文件,第二个 foo remove
会导致 bar
被 deactivate
(停用)。