]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - share/mk/meta2deps.py
login(1): when exporting variables check the result of setenv(3)
[FreeBSD/FreeBSD.git] / share / mk / meta2deps.py
1 #!/usr/bin/env python
2
3 from __future__ import print_function
4
5 """
6 This script parses each "meta" file and extracts the
7 information needed to deduce build and src dependencies.
8
9 It works much the same as the original shell script, but is
10 *much* more efficient.
11
12 The parsing work is handled by the class MetaFile.
13 We only pay attention to a subset of the information in the
14 "meta" files.  Specifically:
15
16 'CWD'   to initialize our notion.
17
18 'C'     to track chdir(2) on a per process basis
19
20 'R'     files read are what we really care about.
21         directories read, provide a clue to resolving
22         subsequent relative paths.  That is if we cannot find
23         them relative to 'cwd', we check relative to the last
24         dir read.
25
26 'W'     files opened for write or read-write,
27         for filemon V3 and earlier.
28
29 'E'     files executed.
30
31 'L'     files linked
32
33 'V'     the filemon version, this record is used as a clue
34         that we have reached the interesting bit.
35
36 """
37
38 """
39 RCSid:
40         $FreeBSD$
41         $Id: meta2deps.py,v 1.34 2020/10/02 03:11:17 sjg Exp $
42
43         Copyright (c) 2011-2020, Simon J. Gerraty
44         Copyright (c) 2011-2017, Juniper Networks, Inc.
45         All rights reserved.
46
47         Redistribution and use in source and binary forms, with or without
48         modification, are permitted provided that the following conditions
49         are met:
50         1. Redistributions of source code must retain the above copyright
51            notice, this list of conditions and the following disclaimer.
52         2. Redistributions in binary form must reproduce the above copyright
53            notice, this list of conditions and the following disclaimer in the
54            documentation and/or other materials provided with the distribution.
55
56         THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
57         "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
58         LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
59         A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
60         OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
61         SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
62         LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
63         DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
64         THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
65         (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
66         OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
67
68 """
69
70 import os, re, sys
71
72 def getv(dict, key, d=None):
73     """Lookup key in dict and return value or the supplied default."""
74     if key in dict:
75         return dict[key]
76     return d
77
78 def resolve(path, cwd, last_dir=None, debug=0, debug_out=sys.stderr):
79     """
80     Return an absolute path, resolving via cwd or last_dir if needed.
81     """
82     if path.endswith('/.'):
83         path = path[0:-2]
84     if len(path) > 0 and path[0] == '/':
85         if os.path.exists(path):
86             return path
87         if debug > 2:
88             print("skipping non-existent:", path, file=debug_out)
89         return None
90     if path == '.':
91         return cwd
92     if path.startswith('./'):
93         return cwd + path[1:]
94     if last_dir == cwd:
95         last_dir = None
96     for d in [last_dir, cwd]:
97         if not d:
98             continue
99         if path == '..':
100             dw = d.split('/')
101             p = '/'.join(dw[:-1])
102             if not p:
103                 p = '/'
104             return p
105         p = '/'.join([d,path])
106         if debug > 2:
107             print("looking for:", p, end=' ', file=debug_out)
108         if not os.path.exists(p):
109             if debug > 2:
110                 print("nope", file=debug_out)
111             p = None
112             continue
113         if debug > 2:
114             print("found:", p, file=debug_out)
115         return p
116     return None
117
118 def cleanpath(path):
119     """cleanup path without using realpath(3)"""
120     if path.startswith('/'):
121         r = '/'
122     else:
123         r = ''
124     p = []
125     w = path.split('/')
126     for d in w:
127         if not d or d == '.':
128             continue
129         if d == '..':
130             try:
131                 p.pop()
132                 continue
133             except:
134                 break
135         p.append(d)
136
137     return r + '/'.join(p)
138
139 def abspath(path, cwd, last_dir=None, debug=0, debug_out=sys.stderr):
140     """
141     Return an absolute path, resolving via cwd or last_dir if needed.
142     this gets called a lot, so we try to avoid calling realpath.
143     """
144     rpath = resolve(path, cwd, last_dir, debug, debug_out)
145     if rpath:
146         path = rpath
147     elif len(path) > 0 and path[0] == '/':
148         return None
149     if (path.find('/') < 0 or
150         path.find('./') > 0 or
151         path.endswith('/..')):
152         path = cleanpath(path)
153     return path
154
155 def sort_unique(list, cmp=None, key=None, reverse=False):
156     list.sort(cmp, key, reverse)
157     nl = []
158     le = None
159     for e in list:
160         if e == le:
161             continue
162         le = e
163         nl.append(e)
164     return nl
165
166 def add_trims(x):
167     return ['/' + x + '/',
168             '/' + x,
169             x + '/',
170             x]
171
172 class MetaFile:
173     """class to parse meta files generated by bmake."""
174
175     conf = None
176     dirdep_re = None
177     host_target = None
178     srctops = []
179     objroots = []
180     excludes = []
181     seen = {}
182     obj_deps = []
183     src_deps = []
184     file_deps = []
185
186     def __init__(self, name, conf={}):
187         """if name is set we will parse it now.
188         conf can have the follwing keys:
189
190         SRCTOPS list of tops of the src tree(s).
191
192         CURDIR  the src directory 'bmake' was run from.
193
194         RELDIR  the relative path from SRCTOP to CURDIR
195
196         MACHINE the machine we built for.
197                 set to 'none' if we are not cross-building.
198                 More specifically if machine cannot be deduced from objdirs.
199
200         TARGET_SPEC
201                 Sometimes MACHINE isn't enough.
202
203         HOST_TARGET
204                 when we build for the pseudo machine 'host'
205                 the object tree uses HOST_TARGET rather than MACHINE.
206
207         OBJROOTS a list of the common prefix for all obj dirs it might
208                 end in '/' or '-'.
209
210         DPDEPS  names an optional file to which per file dependencies
211                 will be appended.
212                 For example if 'some/path/foo.h' is read from SRCTOP
213                 then 'DPDEPS_some/path/foo.h +=' "RELDIR" is output.
214                 This can allow 'bmake' to learn all the dirs within
215                 the tree that depend on 'foo.h'
216
217         EXCLUDES
218                 A list of paths to ignore.
219                 ccache(1) can otherwise be trouble.
220
221         debug   desired debug level
222
223         debug_out open file to send debug output to (sys.stderr)
224
225         """
226
227         self.name = name
228         self.debug = getv(conf, 'debug', 0)
229         self.debug_out = getv(conf, 'debug_out', sys.stderr)
230
231         self.machine = getv(conf, 'MACHINE', '')
232         self.machine_arch = getv(conf, 'MACHINE_ARCH', '')
233         self.target_spec = getv(conf, 'TARGET_SPEC', '')
234         self.curdir = getv(conf, 'CURDIR')
235         self.reldir = getv(conf, 'RELDIR')
236         self.dpdeps = getv(conf, 'DPDEPS')
237         self.line = 0
238
239         if not self.conf:
240             # some of the steps below we want to do only once
241             self.conf = conf
242             self.host_target = getv(conf, 'HOST_TARGET')
243             for srctop in getv(conf, 'SRCTOPS', []):
244                 if srctop[-1] != '/':
245                     srctop += '/'
246                 if not srctop in self.srctops:
247                     self.srctops.append(srctop)
248                 _srctop = os.path.realpath(srctop)
249                 if _srctop[-1] != '/':
250                     _srctop += '/'
251                 if not _srctop in self.srctops:
252                     self.srctops.append(_srctop)
253
254             trim_list = add_trims(self.machine)
255             if self.machine == 'host':
256                 trim_list += add_trims(self.host_target)
257             if self.target_spec:
258                 trim_list += add_trims(self.target_spec)
259
260             for objroot in getv(conf, 'OBJROOTS', []):
261                 for e in trim_list:
262                     if objroot.endswith(e):
263                         # this is not what we want - fix it
264                         objroot = objroot[0:-len(e)]
265
266                 if objroot[-1] != '/':
267                     objroot += '/'
268                 if not objroot in self.objroots:
269                     self.objroots.append(objroot)
270                     _objroot = os.path.realpath(objroot)
271                     if objroot[-1] == '/':
272                         _objroot += '/'
273                     if not _objroot in self.objroots:
274                         self.objroots.append(_objroot)
275
276             # we want the longest match
277             self.srctops.sort(reverse=True)
278             self.objroots.sort(reverse=True)
279
280             self.excludes = getv(conf, 'EXCLUDES', [])
281
282             if self.debug:
283                 print("host_target=", self.host_target, file=self.debug_out)
284                 print("srctops=", self.srctops, file=self.debug_out)
285                 print("objroots=", self.objroots, file=self.debug_out)
286                 print("excludes=", self.excludes, file=self.debug_out)
287
288             self.dirdep_re = re.compile(r'([^/]+)/(.+)')
289
290         if self.dpdeps and not self.reldir:
291             if self.debug:
292                 print("need reldir:", end=' ', file=self.debug_out)
293             if self.curdir:
294                 srctop = self.find_top(self.curdir, self.srctops)
295                 if srctop:
296                     self.reldir = self.curdir.replace(srctop,'')
297                     if self.debug:
298                         print(self.reldir, file=self.debug_out)
299             if not self.reldir:
300                 self.dpdeps = None      # we cannot do it?
301
302         self.cwd = os.getcwd()          # make sure this is initialized
303         self.last_dir = self.cwd
304
305         if name:
306             self.try_parse()
307
308     def reset(self):
309         """reset state if we are being passed meta files from multiple directories."""
310         self.seen = {}
311         self.obj_deps = []
312         self.src_deps = []
313         self.file_deps = []
314
315     def dirdeps(self, sep='\n'):
316         """return DIRDEPS"""
317         return sep.strip() + sep.join(self.obj_deps)
318
319     def src_dirdeps(self, sep='\n'):
320         """return SRC_DIRDEPS"""
321         return sep.strip() + sep.join(self.src_deps)
322
323     def file_depends(self, out=None):
324         """Append DPDEPS_${file} += ${RELDIR}
325         for each file we saw, to the output file."""
326         if not self.reldir:
327             return None
328         for f in sort_unique(self.file_deps):
329             print('DPDEPS_%s += %s' % (f, self.reldir), file=out)
330         # these entries provide for reverse DIRDEPS lookup
331         for f in self.obj_deps:
332             print('DEPDIRS_%s += %s' % (f, self.reldir), file=out)
333
334     def seenit(self, dir):
335         """rememer that we have seen dir."""
336         self.seen[dir] = 1
337
338     def add(self, list, data, clue=''):
339         """add data to list if it isn't already there."""
340         if data not in list:
341             list.append(data)
342             if self.debug:
343                 print("%s: %sAdd: %s" % (self.name, clue, data), file=self.debug_out)
344
345     def find_top(self, path, list):
346         """the logical tree may be split across multiple trees"""
347         for top in list:
348             if path.startswith(top):
349                 if self.debug > 2:
350                     print("found in", top, file=self.debug_out)
351                 return top
352         return None
353
354     def find_obj(self, objroot, dir, path, input):
355         """return path within objroot, taking care of .dirdep files"""
356         ddep = None
357         for ddepf in [path + '.dirdep', dir + '/.dirdep']:
358             if not ddep and os.path.exists(ddepf):
359                 ddep = open(ddepf, 'r').readline().strip('# \n')
360                 if self.debug > 1:
361                     print("found %s: %s\n" % (ddepf, ddep), file=self.debug_out)
362                 if ddep.endswith(self.machine):
363                     ddep = ddep[0:-(1+len(self.machine))]
364                 elif self.target_spec and ddep.endswith(self.target_spec):
365                     ddep = ddep[0:-(1+len(self.target_spec))]
366
367         if not ddep:
368             # no .dirdeps, so remember that we've seen the raw input
369             self.seenit(input)
370             self.seenit(dir)
371             if self.machine == 'none':
372                 if dir.startswith(objroot):
373                     return dir.replace(objroot,'')
374                 return None
375             m = self.dirdep_re.match(dir.replace(objroot,''))
376             if m:
377                 ddep = m.group(2)
378                 dmachine = m.group(1)
379                 if dmachine != self.machine:
380                     if not (self.machine == 'host' and
381                             dmachine == self.host_target):
382                         if self.debug > 2:
383                             print("adding .%s to %s" % (dmachine, ddep), file=self.debug_out)
384                         ddep += '.' + dmachine
385
386         return ddep
387
388     def try_parse(self, name=None, file=None):
389         """give file and line number causing exception"""
390         try:
391             self.parse(name, file)
392         except:
393             # give a useful clue
394             print('{}:{}: '.format(self.name, self.line), end=' ', file=sys.stderr)
395             raise
396
397     def parse(self, name=None, file=None):
398         """A meta file looks like:
399
400         # Meta data file "path"
401         CMD "command-line"
402         CWD "cwd"
403         TARGET "target"
404         -- command output --
405         -- filemon acquired metadata --
406         # buildmon version 3
407         V 3
408         C "pid" "cwd"
409         E "pid" "path"
410         F "pid" "child"
411         R "pid" "path"
412         W "pid" "path"
413         X "pid" "status"
414         D "pid" "path"
415         L "pid" "src" "target"
416         M "pid" "old" "new"
417         S "pid" "path"
418         # Bye bye
419
420         We go to some effort to avoid processing a dependency more than once.
421         Of the above record types only C,E,F,L,R,V and W are of interest.
422         """
423
424         version = 0                     # unknown
425         if name:
426             self.name = name;
427         if file:
428             f = file
429             cwd = self.last_dir = self.cwd
430         else:
431             f = open(self.name, 'r')
432         skip = True
433         pid_cwd = {}
434         pid_last_dir = {}
435         last_pid = 0
436
437         self.line = 0
438         if self.curdir:
439             self.seenit(self.curdir)    # we ignore this
440
441         interesting = 'CEFLRV'
442         for line in f:
443             self.line += 1
444             # ignore anything we don't care about
445             if not line[0] in interesting:
446                 continue
447             if self.debug > 2:
448                 print("input:", line, end=' ', file=self.debug_out)
449             w = line.split()
450
451             if skip:
452                 if w[0] == 'V':
453                     skip = False
454                     version = int(w[1])
455                     """
456                     if version < 4:
457                         # we cannot ignore 'W' records
458                         # as they may be 'rw'
459                         interesting += 'W'
460                     """
461                 elif w[0] == 'CWD':
462                     self.cwd = cwd = self.last_dir = w[1]
463                     self.seenit(cwd)    # ignore this
464                     if self.debug:
465                         print("%s: CWD=%s" % (self.name, cwd), file=self.debug_out)
466                 continue
467
468             pid = int(w[1])
469             if pid != last_pid:
470                 if last_pid:
471                     pid_last_dir[last_pid] = self.last_dir
472                 cwd = getv(pid_cwd, pid, self.cwd)
473                 self.last_dir = getv(pid_last_dir, pid, self.cwd)
474                 last_pid = pid
475
476             # process operations
477             if w[0] == 'F':
478                 npid = int(w[2])
479                 pid_cwd[npid] = cwd
480                 pid_last_dir[npid] = cwd
481                 last_pid = npid
482                 continue
483             elif w[0] == 'C':
484                 cwd = abspath(w[2], cwd, None, self.debug, self.debug_out)
485                 if not cwd:
486                     cwd = w[2]
487                     if self.debug > 1:
488                         print("missing cwd=", cwd, file=self.debug_out)
489                 if cwd.endswith('/.'):
490                     cwd = cwd[0:-2]
491                 self.last_dir = pid_last_dir[pid] = cwd
492                 pid_cwd[pid] = cwd
493                 if self.debug > 1:
494                     print("cwd=", cwd, file=self.debug_out)
495                 continue
496
497             if w[2] in self.seen:
498                 if self.debug > 2:
499                     print("seen:", w[2], file=self.debug_out)
500                 continue
501             # file operations
502             if w[0] in 'ML':
503                 # these are special, tread src as read and
504                 # target as write
505                 self.parse_path(w[2].strip("'"), cwd, 'R', w)
506                 self.parse_path(w[3].strip("'"), cwd, 'W', w)
507                 continue
508             elif w[0] in 'ERWS':
509                 path = w[2]
510                 if path == '.':
511                     continue
512                 self.parse_path(path, cwd, w[0], w)
513
514         assert(version > 0)
515         if not file:
516             f.close()
517
518     def is_src(self, base, dir, rdir):
519         """is base in srctop"""
520         for dir in [dir,rdir]:
521             if not dir:
522                 continue
523             path = '/'.join([dir,base])
524             srctop = self.find_top(path, self.srctops)
525             if srctop:
526                 if self.dpdeps:
527                     self.add(self.file_deps, path.replace(srctop,''), 'file')
528                 self.add(self.src_deps, dir.replace(srctop,''), 'src')
529                 self.seenit(dir)
530                 return True
531         return False
532
533     def parse_path(self, path, cwd, op=None, w=[]):
534         """look at a path for the op specified"""
535
536         if not op:
537             op = w[0]
538
539         # we are never interested in .dirdep files as dependencies
540         if path.endswith('.dirdep'):
541             return
542         for p in self.excludes:
543             if p and path.startswith(p):
544                 if self.debug > 2:
545                     print("exclude:", p, path, file=self.debug_out)
546                 return
547         # we don't want to resolve the last component if it is
548         # a symlink
549         path = resolve(path, cwd, self.last_dir, self.debug, self.debug_out)
550         if not path:
551             return
552         dir,base = os.path.split(path)
553         if dir in self.seen:
554             if self.debug > 2:
555                 print("seen:", dir, file=self.debug_out)
556             return
557         # we can have a path in an objdir which is a link
558         # to the src dir, we may need to add dependencies for each
559         rdir = dir
560         dir = abspath(dir, cwd, self.last_dir, self.debug, self.debug_out)
561         rdir = os.path.realpath(dir)
562         if rdir == dir:
563             rdir = None
564         # now put path back together
565         path = '/'.join([dir,base])
566         if self.debug > 1:
567             print("raw=%s rdir=%s dir=%s path=%s" % (w[2], rdir, dir, path), file=self.debug_out)
568         if op in 'RWS':
569             if path in [self.last_dir, cwd, self.cwd, self.curdir]:
570                 if self.debug > 1:
571                     print("skipping:", path, file=self.debug_out)
572                 return
573             if os.path.isdir(path):
574                 if op in 'RW':
575                     self.last_dir = path;
576                 if self.debug > 1:
577                     print("ldir=", self.last_dir, file=self.debug_out)
578                 return
579
580         if op in 'ER':
581             # finally, we get down to it
582             if dir == self.cwd or dir == self.curdir:
583                 return
584             if self.is_src(base, dir, rdir):
585                 self.seenit(w[2])
586                 if not rdir:
587                     return
588
589             objroot = None
590             for dir in [dir,rdir]:
591                 if not dir:
592                     continue
593                 objroot = self.find_top(dir, self.objroots)
594                 if objroot:
595                     break
596             if objroot:
597                 ddep = self.find_obj(objroot, dir, path, w[2])
598                 if ddep:
599                     self.add(self.obj_deps, ddep, 'obj')
600                     if self.dpdeps and objroot.endswith('/stage/'):
601                         sp = '/'.join(path.replace(objroot,'').split('/')[1:])
602                         self.add(self.file_deps, sp, 'file')
603             else:
604                 # don't waste time looking again
605                 self.seenit(w[2])
606                 self.seenit(dir)
607
608
609 def main(argv, klass=MetaFile, xopts='', xoptf=None):
610     """Simple driver for class MetaFile.
611
612     Usage:
613         script [options] [key=value ...] "meta" ...
614
615     Options and key=value pairs contribute to the
616     dictionary passed to MetaFile.
617
618     -S "SRCTOP"
619                 add "SRCTOP" to the "SRCTOPS" list.
620
621     -C "CURDIR"
622
623     -O "OBJROOT"
624                 add "OBJROOT" to the "OBJROOTS" list.
625
626     -m "MACHINE"
627
628     -a "MACHINE_ARCH"
629
630     -H "HOST_TARGET"
631
632     -D "DPDEPS"
633
634     -d  bumps debug level
635
636     """
637     import getopt
638
639     # import Psyco if we can
640     # it can speed things up quite a bit
641     have_psyco = 0
642     try:
643         import psyco
644         psyco.full()
645         have_psyco = 1
646     except:
647         pass
648
649     conf = {
650         'SRCTOPS': [],
651         'OBJROOTS': [],
652         'EXCLUDES': [],
653         }
654
655     try:
656         machine = os.environ['MACHINE']
657         if machine:
658             conf['MACHINE'] = machine
659         machine_arch = os.environ['MACHINE_ARCH']
660         if machine_arch:
661             conf['MACHINE_ARCH'] = machine_arch
662         srctop = os.environ['SB_SRC']
663         if srctop:
664             conf['SRCTOPS'].append(srctop)
665         objroot = os.environ['SB_OBJROOT']
666         if objroot:
667             conf['OBJROOTS'].append(objroot)
668     except:
669         pass
670
671     debug = 0
672     output = True
673
674     opts, args = getopt.getopt(argv[1:], 'a:dS:C:O:R:m:D:H:qT:X:' + xopts)
675     for o, a in opts:
676         if o == '-a':
677             conf['MACHINE_ARCH'] = a
678         elif o == '-d':
679             debug += 1
680         elif o == '-q':
681             output = False
682         elif o == '-H':
683             conf['HOST_TARGET'] = a
684         elif o == '-S':
685             if a not in conf['SRCTOPS']:
686                 conf['SRCTOPS'].append(a)
687         elif o == '-C':
688             conf['CURDIR'] = a
689         elif o == '-O':
690             if a not in conf['OBJROOTS']:
691                 conf['OBJROOTS'].append(a)
692         elif o == '-R':
693             conf['RELDIR'] = a
694         elif o == '-D':
695             conf['DPDEPS'] = a
696         elif o == '-m':
697             conf['MACHINE'] = a
698         elif o == '-T':
699             conf['TARGET_SPEC'] = a
700         elif o == '-X':
701             if a not in conf['EXCLUDES']:
702                 conf['EXCLUDES'].append(a)
703         elif xoptf:
704             xoptf(o, a, conf)
705
706     conf['debug'] = debug
707
708     # get any var=val assignments
709     eaten = []
710     for a in args:
711         if a.find('=') > 0:
712             k,v = a.split('=')
713             if k in ['SRCTOP','OBJROOT','SRCTOPS','OBJROOTS']:
714                 if k == 'SRCTOP':
715                     k = 'SRCTOPS'
716                 elif k == 'OBJROOT':
717                     k = 'OBJROOTS'
718                 if v not in conf[k]:
719                     conf[k].append(v)
720             else:
721                 conf[k] = v
722             eaten.append(a)
723             continue
724         break
725
726     for a in eaten:
727         args.remove(a)
728
729     debug_out = getv(conf, 'debug_out', sys.stderr)
730
731     if debug:
732         print("config:", file=debug_out)
733         print("psyco=", have_psyco, file=debug_out)
734         for k,v in list(conf.items()):
735             print("%s=%s" % (k,v), file=debug_out)
736
737     m = None
738     for a in args:
739         if a.endswith('.meta'):
740             if not os.path.exists(a):
741                 continue
742             m = klass(a, conf)
743         elif a.startswith('@'):
744             # there can actually multiple files per line
745             for line in open(a[1:]):
746                 for f in line.strip().split():
747                     if not os.path.exists(f):
748                         continue
749                     m = klass(f, conf)
750
751     if output and m:
752         print(m.dirdeps())
753
754         print(m.src_dirdeps('\nsrc:'))
755
756         dpdeps = getv(conf, 'DPDEPS')
757         if dpdeps:
758             m.file_depends(open(dpdeps, 'wb'))
759
760     return m
761
762 if __name__ == '__main__':
763     try:
764         main(sys.argv)
765     except:
766         # yes, this goes to stdout
767         print("ERROR: ", sys.exc_info()[1])
768         raise
769