1#!@PYTHON@
2#
3# createmodule.py - Takes the name of a environment init script and
4# produces a modulefile that duplicates the changes made by the init script
5#
6# Copyright (C) 2012 by Orion E. Poplawski <orion@cora.nwra.com>
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 2 of the License, or
11# (at your option) any later version.
12
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17
18# You should have received a copy of the GNU General Public License
19# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20from __future__ import print_function
21
22from optparse import OptionParser
23import os,sys,re
24from subprocess import *
25import platform
26
27# Handle options
28usage = "Usage: %prog [-p prefix] <initscript> [args]"
29parser = OptionParser()
30parser.set_usage(usage)
31parser.add_option('-p', '--prefix', action='store', type='string', dest='prefix', help='Specify path prefix')
32parser.add_option('--noprefix', action='store_true', dest='noprefix', default=False, help='Do not generate a prefix')
33(options, args) = parser.parse_args()
34
35# Need a script name
36if not args:
37    parser.print_usage()
38    exit(1)
39
40# Determine if running environment is based on cmd.exe or not
41def iscmdshell():
42    return True if platform.system() == 'Windows' else False
43
44# Return environment after a command
45def getenv(cmd = ':'):
46    env = {}
47    if iscmdshell():
48        # ':' command not supported by cmd.exe
49        cmd = (cmd if cmd != ':' else '@echo off') + " >nul & set"
50    else:
51        cmd = cmd + " >/dev/null;env"
52    p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, universal_newlines=True)
53    (stdout, stderr) = p.communicate()
54    if p.returncode != 0:
55        print("ERROR: Could not execute initscript:")
56        print("%s returned exit code %d" % (cmd, p.returncode))
57        print(stderr)
58        exit(1)
59    if stderr != '':
60        print("WARNING: initscript sent the following to stderr:")
61        print(stderr)
62    # Parse the output key=value pairs
63    skip = False
64    for line in stdout.splitlines():
65        if skip:
66            if line == '}':
67                skip = False
68            continue
69        elif iscmdshell() and line.find('=') == -1:
70            continue
71        try:
72            (var,value) = line.split('=',1)
73        except ValueError:
74            print("ERROR: Could not parse output line:")
75            print(line)
76            exit(1)
77        # Exported functions - not handled
78        if value.find('() {') == 0:
79            skip = True
80        else:
81            env[var] = value
82    return env
83
84#Record initial environment
85env1=getenv()
86
87#Record environment after sourcing the initscript
88if iscmdshell():
89    if len(args)>1:
90        env2=getenv('"' + args[0] + '" ' + " ".join(args[1:]))
91    else:
92        env2=getenv('"' + args[0] + '"')
93else:
94    env2=getenv(". " + " ".join(args))
95
96# Initialize our variables for storing modifications
97chdir = None
98appendpath = {}
99prependpath = {}
100unhandled = {}
101setenv = {}
102unsetenv = []
103pathnames = []
104
105# Function to nomalize all paths in a list of paths and remove duplicate items
106def normpaths(paths):
107    newpaths = []
108    for path in paths:
109        normpath = os.path.normpath(path)
110        if normpath not in newpaths and normpath != '.':
111             newpaths.append(os.path.normpath(path))
112    return newpaths
113
114# Start with existing keys and look for changes
115for key in env1.keys():
116    # Test for delete
117    if key not in env2:
118        unsetenv.append(key)
119        continue
120    # No change
121    if env1[key] == env2[key]:
122        del env2[key]
123        continue
124    #Working directory change
125    if key == 'PWD':
126        chdir=os.path.normpath(env2[key])
127        pathnames.append(chdir)
128        del env2[key]
129        continue
130    # Determine modifcations to beginning and end of the string
131    try:
132        (prepend,append) = env2[key].split(env1[key])
133    except ValueError:
134         continue
135    if prepend:
136        presep = prepend[-1:]
137        prependpaths = prepend.strip(presep).split(presep)
138        # LICENSE variables often include paths outside install directory
139        if 'LICENSE' not in key:
140            pathnames += prependpaths
141        if presep not in prependpath:
142            prependpath[presep] = {}
143        newpath = presep.join(normpaths(prependpaths))
144        if newpath:
145            prependpath[presep][key] = newpath
146        else:
147            unhandled[key] = env2[key]
148    if append:
149        appsep = append[0:1]
150        appendpaths = append.strip(appsep).split(appsep)
151        # LICENSE variables often include paths outside install directory
152        if 'LICENSE' not in key:
153            pathnames += appendpaths
154        if appsep not in appendpath:
155            appendpath[appsep] = {}
156        newpath = appsep.join(normpaths(appendpaths))
157        if newpath:
158            appendpath[appsep][key] = newpath
159        else:
160            unhandled[key] = env2[key]
161    del env2[key]
162
163# We're left with new keys in env2
164for key in env2.keys():
165    # Use prepend-path for new paths
166    if (re.search('(DIRS|FILES|PATH)$',key)) or (':' in env2[key]):
167        prependpaths = env2[key].strip(':').split(':')
168        # MANPATH can have system defaults added it it wasn't previously set
169        # LICENSE variables often include paths outside install directory
170        if key != 'MANPATH' and 'LICENSE' not in key:
171            pathnames += prependpaths
172        if ':' not in prependpath:
173            prependpath[':'] = {}
174        prependpath[':'][key] = ':'.join(normpaths(prependpaths))
175        continue
176    # Set new variables
177    setenv[key] = os.path.normpath(env2[key])
178    if 'LICENSE' not in key:
179        pathnames.append(setenv[key])
180
181# Report unhandled keys
182for key in unhandled.keys():
183    print("Unhandled change of", key, file=sys.stderr)
184    print("Before <%s>" % env1[key], file=sys.stderr)
185    print("After <%s>" % unhandled[key], file=sys.stderr)
186    for sepkey in appendpath.keys():
187        appendpath[sepkey].pop(key, None)
188    for sepkey in prependpath.keys():
189        prependpath[sepkey].pop(key, None)
190
191# Determine a prefix
192prefix=None
193if options.prefix:
194    prefix = options.prefix
195elif not options.noprefix:
196    prefix = os.path.commonprefix(pathnames).rstrip('/')
197    if prefix == '':
198          prefix = None
199
200# Print out the modulefile
201print("#%Module 1.0")
202
203# Prefix
204if prefix is not None:
205    print("\nset prefix " + prefix + "\n")
206
207# Chdir
208if chdir is not None:
209    print("chdir\t" + chdir)
210
211# Function to format output line with tabs and substituting prefix
212def formatline(item, key, value=None):
213    print(item, end=' ')
214    print("\t"*(2-(len(item)+1)//8), end=' ')
215    print(key, end=' ')
216    if value is not None:
217        print("\t"*(3-(len(key)+1)//8), end=' ')
218        if prefix is not None:
219            # Prefer usage of regular expression to perform a none
220            # case-sensitive substitution (cygwin vs cmd.exe)
221            if iscmdshell():
222                print(re.sub('(?i)' + re.escape(prefix), '$prefix', value))
223            else:
224                print(value.replace(prefix,'$prefix'))
225        else:
226            print(value)
227
228# Paths first, grouped by variable name
229for sepkey in prependpath.keys():
230    pathkeys = list(prependpath[sepkey].keys())
231    pathkeys.sort()
232    for key in pathkeys:
233        if sepkey == ":":
234            formatline("prepend-path",key,prependpath[sepkey][key])
235        else:
236            formatline("prepend-path --delim %s" % sepkey,key,prependpath[sepkey][key])
237
238for sepkey in appendpath.keys():
239    pathkeys = list(appendpath[sepkey].keys())
240    pathkeys.sort()
241    for key in pathkeys:
242        if sepkey == ":":
243            formatline("append-path",key,appendpath[sepkey][key])
244        else:
245            formatline("append-path --delim %s" % sepkey,key,appendpath[sepkey][key])
246
247# Setenv
248setenvkeys = list(setenv.keys())
249setenvkeys.sort()
250if setenvkeys:
251    print()
252for key in setenvkeys:
253    formatline("setenv",key,setenv[key])
254
255# Unsetenv
256unsetenv.sort()
257if unsetenv:
258    print()
259for key in unsetenv:
260    formatline("unsetenv",key)
261