how to load virtualenv using environmental module file (tcl script)?

4.8k Views Asked by At

I am trying to write a module file for a program that creates a python virtualenv. In order to start the virtualenv, it needs to first run /programs/program-env/bin/activate. How do I do this in a modulefile? Any help will be greatly appreciated.

Note: I tried just putting the above line in the file and it didn't work.

Thanks,

Edit:

I am writing a modulefile to load a program that can only run in a virtualenv. Normally these modulefiles will set variable names and/or add bin directory to path. Since the above package is somewhat different, I don't know how to proceed. An example module file can be found here.

4

There are 4 best solutions below

0
On

Here is a slightly more complete answer, building on the answers by Donal and betapatch, that allows you to swap between two modules which do similar things:

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;"
}

Firstly, you need to use source .../activate rather than just .../activate.

Secondly, modules has some horrible logic when swapping modules. If you want to module swap foo bar (remove foo and load bar in its place), it actually does the following:

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

This means that if foo and bar are both modulefiles using virtualenvs, the second foo remove will deactivate bar.

0
On

The Modules system is pretty strange, since what it's really doing is creating a set of instructions that are evaluated by the calling shell. This means that normal Tcl ways of doing things are often not quite right; it is the caller who needs to run /programs/program-env/bin/activate, not the Tcl script.

The first thing to try is:

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

However, looking between the lines in the FAQ, I see that you probably would need to do this (with guards):

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

I have no idea how to reverse the operation (which is part of the point of a module).

2
On

You have not explained very clearly what you are trying to do but given your mention of a tcl script in the title I will assume you are writing a Tcl script that needs to load the virtualenv environment to manipulate python script using the virtualenv configuration. The activate scripts are bash scripts that ultimately setup the current environment. You cannot simply source these into Tcl as Tcl is not a Bourne shell. However you can create a shell subprocess and read its environment and compare this with the environment as changed following sourcing the activate script. If your tcl script applies the differences to its own environment the resulting Tcl process will be equivalent to the bash shell after sourcing the activate script.

Here is an example. If your run this as tclsh scriptname bin/activate it prints the environment which will now include the additional settings from the activate script. In my test on a linux box this added a VIRTUAL_ENV variable and modified PS1 and 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
}
0
On

Based on Donal Fellows answer and the docs it can be done with:

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

The semicolon is essential.