ENH: Update FreeFOAM contributions to GPL v3
[freefoam.git] / data / python / FreeFOAM / tutorial.py
blob661894787ff5ce3b9910494cc0d991cedb7c5692
1 #-------------------------------------------------------------------------------
2 # ______ _ ____ __ __
3 # | ____| _| |_ / __ \ /\ | \/ |
4 # | |__ _ __ ___ ___ / \| | | | / \ | \ / |
5 # | __| '__/ _ \/ _ ( (| |) ) | | |/ /\ \ | |\/| |
6 # | | | | | __/ __/\_ _/| |__| / ____ \| | | |
7 # |_| |_| \___|\___| |_| \____/_/ \_\_| |_|
9 # FreeFOAM: The Cross-Platform CFD Toolkit
11 # Copyright (C) 2008-2012 Michael Wild <themiwi@users.sf.net>
12 # Gerber van der Graaf <gerber_graaf@users.sf.net>
13 #-------------------------------------------------------------------------------
14 # License
15 # This file is part of FreeFOAM.
17 # FreeFOAM is free software: you can redistribute it and/or modify it
18 # under the terms of the GNU General Public License as published by the
19 # Free Software Foundation, either version 3 of the License, or (at your
20 # option) any later version.
22 # FreeFOAM is distributed in the hope that it will be useful, but WITHOUT
23 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
24 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
25 # for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with FreeFOAM. If not, see <http://www.gnu.org/licenses/>.
30 # Description
31 # Useful Python functionality to run FreeFOAM tutorial cases
33 #-------------------------------------------------------------------------------
35 """Classes and functions useful for running FreeFOAM tutorial applications."""
37 # want to be future proof
38 from FreeFOAM.compat import *
40 import os as _os
41 import os.path as _op
42 import FreeFOAM as _f
43 import FreeFOAM.run as _fr
44 import FreeFOAM.util as _fu
46 compile_config = _f.BUILD_TYPE
48 class ConfiguringFailedError(Exception):
49 """Thrown if configuring a tutorial application failed."""
50 def __init__(self, bindir, logfile):
51 """Initialize the exception object.
53 Parameters
54 ----------
55 bindir : The binary directory.
56 logfile : Name of the logfile which marks the case as run.
58 """
59 Exception.__init__(self, bindir, logfile)
61 def __str__(self):
62 """Return string representation of the error."""
63 return ('*** ERROR *** Configuring application %s failed.\n' +
64 'Refer to the logfile %s for more information.')%self.args
66 class CompilingFailedError(Exception):
67 """Thrown if the compilation of a tutorial application failed."""
68 def __init__(self, bindir, logfile):
69 """Initialize the exception object.
71 Parameters
72 ----------
73 bindir : The binary directory where the compilation should have run.
74 logfile : Name of the logfile which marks the case as run.
76 """
77 Exception.__init__(self, bindir, logfile)
79 def __str__(self):
80 """Return string representation of the error."""
81 return ('*** ERROR *** Cleaning application %s failed.\n' +
82 'Refer to the logfile %s for more information.')%self.args
84 class CleaningFailedError(Exception):
85 """Thrown if the cleaning of a tutorial application failed."""
86 def __init__(self, bindir):
87 """Initialize the exception object.
89 Parameters
90 ----------
91 app : The application that was already run.
92 logfile : Name of the logfile which marks the case as run.
94 """
95 Exception.__init__(self, bindir)
97 def __str__(self):
98 """Return string representation of the error."""
99 return ('*** ERROR *** Cleaning application %s failed.'%self.args)
101 class CaseRunner(object):
102 """A case runner.
104 Attributes
105 ----------
106 name : Name of the case.
107 case_dir : Directory containing the case.
108 test_dir : Directory to use in test runs.
109 default_schemes : Use the default finite volume schemes in test runs.
110 Defaults to False.
113 def __init__(self, name, case_dir='.', test_dir=None, skip_test=False):
114 """Initializes a new CaseRunner instance.
116 Parameters
117 ----------
118 name : Name of the case.
119 case_dir : Directory containing the case. Defaults to
120 `os.getcwd()`.
121 test_dir : Directory to create when cloning the case for the test
122 run. Defaults to `case_dir-test`.
123 skip_test : Default value to use when adding a new step.
126 self.name = name
127 self.case_dir = _op.abspath(case_dir)
128 if test_dir == None:
129 test_dir = self.case_dir+'-test'
130 self.test_dir = _op.abspath(test_dir)
131 self.skip_test = skip_test
132 self.default_schemes = False
133 self._stepnames = []
134 self._steps = {}
136 def name(self):
137 """Returns the name of this tutorial."""
138 return self._name
140 def steps(self):
141 """Retrieves the name of all registered steps."""
142 return self._stepnames
144 def add_step(self, stepname, cmd, stamp_file=None, skip_test=None):
145 """Registers a callable object to be run as a tutorial step.
147 Parameters
148 ----------
149 stepname : Name of the step.
150 cmd : Callable (function or object with __call__ function).
151 stamp_file : File to use as a stamp. If absolute, relative path to
152 `self.case_dir` will be used, and then appended to either
153 `self.case_dir` or `self.test_dir`. Defaults to
154 `'log.'+stepname`.
155 skip_test : Skip this step in test mode. Defaults to `self.skip_test`.
157 Note
158 ----
159 `cmd` must have the following signature:
160 def command(case_dir, stamp_file, test_mode):
161 # execute step
162 # write stamp file
163 return result
164 where `case_dir` is the the absolute path to where the case is contained
165 in. `stamp_file` is a `file` object the command should write its log to.
166 The last line must either read `REPORT: SUCCESS` or `REPORT: FAILURE` (as
167 e.g. created by `RunApp.__call__()`). Upon success, the function returns
168 True, False otherwise.
171 assert stepname not in self._stepnames, (
172 "Step "+stepname+" already exists")
173 if stamp_file == None:
174 stamp_file = 'log.'+stepname
175 if _op.isabs(stamp_file):
176 stamp_file = _op.relpath(stamp_file, self.case_dir)
177 if skip_test == None:
178 skip_test = self.skip_test
179 self._stepnames.append(stepname)
180 self._steps[stepname] = {
181 'cmd' : cmd,
182 'stamp' : stamp_file,
183 'skip_test' : skip_test,
186 def add_app_step(self, stepname, app=None, parallel=False, args=[],
187 stamp_file=None, skip_test=None):
188 """Convenience function to add a step running a FreeFOAM application.
190 Parameters
191 ----------
192 stepname : Name of the step.
193 app : Name of the application to run. Defaults to `stepname`.
194 parallel : If True, run the application in parallel.
195 args : Additional arguments to be passed to the application.
196 stamp_file : File to use as a stamp. Relative to `self.case_dir`.
197 Defaults to `'log.'+app`.
198 skip_test : Skip this step in test mode.
201 if app == None:
202 app = stepname
203 if stamp_file == None:
204 stamp_file = 'log.'+_op.basename(stepname)
205 self.add_step(stepname, RunApp(app, parallel, args),
206 stamp_file=stamp_file, skip_test=skip_test)
208 def run(self, step=None, test_mode=False, verbose=False):
209 """Run the case as a whole or a specific step.
211 A step is only run if at least one of the following applies:
212 * `test_mode` is True.
213 * The stamp file of the test (usually the log file) does not exist.
214 * The stamp file of the preceeding step is newer than the one for this
215 step.
217 Every step is assumed to depend on the preceeding step, and above
218 criteria propagate up to the first step if required (except for
219 `test_mode` which only applies to the specified step, or to all steps if
220 `step` is None). This means that if you specify a step to be run and any
221 of the preceeding steps has not been run or is out of date, that step and
222 all the ones in between will also be run.
224 Parameters
225 ----------
226 step : Name of the step to be run. If `None`, try to run all steps.
227 test_mode : Run in testing mode.
228 verbose : Echo the output to standard output.
230 Returns
231 -------
232 True if the steps all ran successfully, False otherwise.
235 import sys
236 assert len(self._stepnames) > 0, "No steps registered"
238 if step == None:
239 step = self._stepnames[-1]
240 assert step in self._stepnames, "Step "+step+" not registered"
242 if test_mode:
243 self.clone_test()
244 case_dir = self.test_dir
245 else:
246 case_dir = self.case_dir
247 case_dir = _op.abspath(case_dir)
249 echo('Running case '+case_dir)
251 stat = True
252 fmt = ' %%-%ds ${HIDE_CURSOR}'%max(
253 30, max(map(len, self._stepnames)))
254 last = self._stepnames.index(step)
255 try:
256 for i, s in enumerate(self._stepnames):
257 if not verbose:
258 _fu.cecho(fmt%s, end='')
259 sys.stdout.flush()
260 stamp = _op.join(case_dir, self._steps[s]['stamp'])
261 do_skip = False
262 if test_mode and self._steps[s]['skip_test']:
263 do_skip = True
264 else:
265 if i > last:
266 do_skip = True
267 elif _op.isfile(stamp):
268 do_skip = True
269 if i > 0:
270 old_s = self._stepnames[i-1]
271 old_stamp = _op.join(
272 case_dir, self._steps[old_s]['stamp'])
273 if _op.getmtime(old_stamp) > _op.getmtime(stamp):
274 do_skip = False
275 if do_skip:
276 if not verbose:
277 _fu.cecho('${YELLOW}[ SKIP ]${NORMAL}')
278 continue
280 try:
281 if verbose:
282 stamp_file = _fu.Tee(stamp, 'wt')
283 else:
284 stamp_file = open(stamp, 'wt')
285 stat = self._steps[s]['cmd'](case_dir, stamp_file, test_mode)
286 if not verbose:
287 if stat:
288 _fu.cecho('${GREEN}[ OK ]${NORMAL}')
289 else:
290 _fu.cecho('${RED}${BOLD}[FAILED]${NORMAL}')
291 except BaseException, e:
292 if not verbose:
293 _fu.cecho('${RED}${BOLD}[FAILED]${NORMAL}')
294 _fu.cerror("Step %s in case %s raised an exception:\n%s"%
295 (s, case_dir, str(e)), file=sys.stderr)
296 stat = False
297 raise
298 if not stat:
299 break
300 finally:
301 _fu.cecho('${SHOW_CURSOR}', end='')
302 return stat
304 def clean(self):
305 """Cleans the case.
307 This function is supposed to be overridden by tutorials requiring more
308 cleaning up than `FreeFOAM.util.clean_case` provides.
310 Note that this implementation also removes `test_dir` if it exist
311 and is not equal to the `case_dir`.
314 _fu.clean_case(self.case_dir)
315 if self.test_dir != self.case_dir and _op.isdir(self.test_dir):
316 _fu.remove_case(self.test_dir)
318 def clone_test(self):
319 """Creates a testing clone.
321 This function is supposed to be overridden by tutorials requiring more
322 cloning work than `FreeFOAM.util.clone()` and `modify_for_test()`
323 provide.
326 if _op.isdir(self.test_dir):
327 _fu.remove_case(self.test_dir)
328 _fu.clone_case(self.case_dir, self.test_dir)
329 self.modify_for_test()
331 def modify_for_test(self):
332 """Modifies the case `test_dir` for use as a test case."""
333 self.modify_global_controlDict_for_test(self.test_dir)
334 self.modify_case_for_test(self.test_dir)
336 def modify_global_controlDict_for_test(self, test_dir):
337 """Copies the global etc directory and modifies if for testing."""
338 ctrld = _op.join(test_dir, 'etc', 'controlDict')
339 if not _op.isdir(_op.dirname(ctrld)):
340 _os.makedirs(_op.dirname(ctrld))
341 _fu.copytree(_op.join(_f.CONFIG_DIR, 'controlDict'), ctrld)
342 _fu.copytree(
343 _op.join(_f.CONFIG_DIR, 'cellModels'),
344 _op.join(test_dir, 'etc', 'cellModels'))
345 _fu.copytree(
346 _op.join(_f.CONFIG_DIR, 'thermoData'),
347 _op.join(test_dir, 'etc', 'thermoData'))
348 modify_file(ctrld, [
349 (r'(fvSchemes\s*)([0-9]);', r'\g<1>1;'),
350 (r'(solution\s*)([0-9]);', r'\g<1>1;') ])
352 def modify_case_for_test(self, test_dir):
353 "Modifies system/{controlDict,fvSchemes,snappyHexMeshDict} for testing."
354 import re
355 # modify system/controlDict
356 for e in ['', '.1st']:
357 ctrld = _op.join(test_dir, 'system', 'controlDict'+e)
358 if _op.isfile(ctrld):
359 modify_file(ctrld, [
360 (r'(startFrom\s+)([a-zA-Z]+);', r'\g<1>latestTime;'),
361 (r'(stopAt\s+)([a-zA-Z]*);', r'\g<1>nextWrite;'),
362 (r'(writeControl\s+)([a-zA-Z]+);', r'\g<1>timeStep;'),
363 (r'(writeInterval\s+)([0-9a-zA-Z.-]+);', r'\g<1>1;'),
364 ], '.orig')
365 # modify system/fvSchemes
366 if self.default_schemes:
367 fv_schemes = (
368 'gradScheme',
369 'divScheme',
370 'laplacianScheme',
371 'interpolationScheme',
372 'snGradScheme',
373 'fluxRequired',
375 for parent, dirs, files in _os.walk(_op.join(test_dir,'system')):
376 if 'fvSchemes' in files:
377 fvs = _op.join(parent, 'fvSchemes')
378 lines = open(fvs, 'rt').readlines()
379 kill_lines = []
380 n_brace = 0
381 search_start = False
382 search_end = False
383 for i, l in enumerate(lines):
384 m = re.search('|'.join(fv_schemes), l)
385 if n_brace > 0 or search_start or m:
386 kill_lines.append(i)
387 if m:
388 n_brace = 0
389 search_start = True
390 search_end = False
391 if search_start or search_end:
392 n_brace += l.count('{')
393 if n_brace > 0:
394 search_start = False
395 search_end = True
396 n_brace -= l.count('}')
397 if search_end and n_brace < 1:
398 search_end = False
399 for i in reversed(kill_lines):
400 del lines[i]
401 f = open(fvs, 'wt')
402 f.writelines(lines)
403 f.write("""
404 gradSchemes { default Gauss linear; }
405 divSchemes
407 default Gauss linear;
408 div(phi,fu_ft_h) Gauss multivariateSelection
410 fu upwind;
411 ft upwind;
412 h upwind;
414 div(phi,ft_b_h_hu) Gauss multivariateSelection
416 fu upwind;
417 ft upwind;
418 bprog upwind;
419 h upwind;
420 hu upwind;
423 laplacianSchemes { default Gauss linear corrected; }
424 interpolationSchemes { default linear; }
425 snGradSchemes { default corrected; }
426 fluxRequired { default yes; }
427 """ )
428 f.close()
430 class ClonedCaseRunner(CaseRunner):
431 """A runner for cases that need to be cloned from another case."""
432 def __init__(self, name, parent_runner, case_dir='.',
433 test_dir=None, skip_test=False):
434 CaseRunner.__init__(self, name, case_dir=case_dir,
435 test_dir=test_dir, skip_test=skip_test)
436 self._parent_runner = parent_runner
438 def clone_from_parent(self, case_dir):
439 """Clones the case from its parent case."""
440 try:
441 parent_dir = self._parent_runner.case_dir
442 if not _op.isdir(_op.join(case_dir, 'system')):
443 _fu.clone_case(parent_dir, case_dir, verbose=False)
444 return True
445 except Exception, e:
446 import sys
447 _fu.cerror("Failed to clone", parent_dir, ":", str(e),
448 file=sys.stderr)
449 return False
451 def run(self, step=None, test_mode=False, verbose=False):
452 """Refer to `CaseRunner.run`."""
453 if not test_mode:
454 self.clone_from_parent(self.case_dir)
455 return CaseRunner.run(self, step=step, test_mode=test_mode,
456 verbose=verbose)
458 def clean(self):
459 """Removes the cloned case and, if it exists, the test-case."""
460 _fu.remove_case(self.case_dir)
461 if _op.isdir(self.test_dir):
462 _fu.remove_case(self.test_dir)
464 def clone_test(self):
465 """Refer to `CaseRunner.clean`."""
466 if _op.isdir(self.test_dir):
467 _fu.remove_case(self.test_dir)
468 self.clone_from_parent(self.test_dir)
469 self.modify_for_test()
471 class TutorialRunner(object):
472 """A tutorial case runner.
474 Attributes
475 ----------
476 default_schemes : Use the default finite volume schemes in test runs.
477 Defaults to False.
478 keep_going : Do not abort after a failed case. Defaults to False.
479 test_mode : Run in testing mode. Defaults to False.
480 verbose : Echo the output to standard output. Defaults to False.
483 def __init__(self, default_schemes=False, keep_going=False, test_mode=False,
484 verbose=False):
485 """Initializes a new TutorialRunner instance."""
486 self.default_schemes = default_schemes
487 self.keep_going = keep_going
488 self.test_mode = test_mode
489 self.verbose = verbose
490 self.did_run = False
491 self._cases = []
493 def cases(self):
494 """Returns a tuple of all registered case names."""
495 return tuple([c.name for c in self._cases])
497 def steps(self):
498 """Returns a tuple of all registered step names."""
499 steps = []
500 for c in self._cases:
501 for s in c.steps():
502 steps.append('.'.join((c.name,s)))
503 return steps
505 def add_case(self, case):
506 """Adds a case."""
507 assert case.name not in self.cases(), (
508 "Trying to re-register case "+case.name)
509 self._cases.append(case)
511 def run(self, step=None):
512 """Run the case as a whole or a specific step.
514 A step is only run if at least one of the following applies:
515 * `self.test_mode` is True.
516 * The stamp file of the test (usually the log file) does not exist.
517 * The stamp file of the preceeding step of the same case is newer than
518 the one for this step.
520 Every step is assumed to depend on the preceeding step of the same case,
521 and above criteria propagate up to the first step if required (except for
522 `self.test_mode` which only applies to the specified step, or to all
523 steps if `step` is None). This means that if you specify a step to be run
524 and any of the preceeding steps of the same case has not been run or is
525 out of date, that step and all the ones in between will also be run.
527 In test mode all steps belonging to the same case are run (up to the
528 specified one), except in the default case, where all steps in all cases
529 are run.
531 The names of the steps are composed of the case name and the step name,
532 separated by a dot. Only specifying the case name is equivalent to
533 running all the steps belonging to that case.
535 Parameters
536 ----------
537 step : Name of the step to be run. If `None`, try to run all steps.
539 Returns
540 -------
541 True if the steps all ran successfully, False otherwise.
544 case = None
545 substep = None
546 if step != None:
547 case_and_step = step.split('.', 1)
548 case = case_and_step[0]
549 if len(case_and_step) > 1:
550 substep = case_and_step[1]
551 cases = self.cases()
552 if case:
553 cases = [case]
554 stat = False
555 for c in filter(lambda c: c.name in cases, self._cases):
556 c.default_schemes = self.default_schemes
557 stat = False
558 try:
559 stat = c.run(substep, self.test_mode, self.verbose)
560 except:
561 if not self.keep_going:
562 raise
563 if not stat and not self.keep_going:
564 break
565 return stat or self.keep_going
567 def clean(self):
568 """Clean all cases."""
569 for c in self._cases:
570 c.clean()
572 def main(self):
573 """Run as the main function in a script.
574 <step> Optionally, a step can be specifiedo which should be
575 run. By default all steps are run. Refer to run() for
576 the details.
579 import optparse
580 import sys
581 parser = optparse.OptionParser(usage='Usage: %prog [options] [step]')
582 parser.add_option('-t', '--test', dest='testmode', action='store_true',
583 default=False, help=('Run in testing mode. This causes the case ' +
584 'to be cloned to a test directory.'))
585 parser.add_option('-k', '--keep-going', dest='keep_going',
586 action='store_true', default=False,
587 help='Don\'t abort after a failed case.')
588 parser.add_option('-d', '--default-schemes', dest='default_schemes',
589 action='store_true', default=False,
590 help='Run using a set of default fvSchemes.')
591 parser.add_option('-l', '--list', dest='list', action='store_true',
592 default=False, help='List the individual steps in this tutorial.')
593 parser.add_option('-c', '--cases', dest='cases', action='store_true',
594 default=False, help='Print the cases in this tutorial.')
595 parser.add_option('--clean', dest='clean', action='store_true',
596 default=False, help='Clean this tutorial case.')
597 parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
598 default=False, help=('Echo the output to standard output'))
600 (opts, args) = parser.parse_args()
601 if opts.list and (opts.testmode or opts.default_schemes or
602 opts.keep_going or opts.cases or opts.clean or opts.verbose or
603 len(args)>0):
604 parser.error(
605 'Option --list may not be used with arguments and other options')
606 if opts.cases and (opts.testmode or opts.default_schemes or
607 opts.keep_going or opts.list or opts.clean or opts.verbose or
608 len(args)>0):
609 parser.error(
610 'Option --cases may not be used with arguments and other options')
611 if opts.clean and (opts.testmode or opts.default_schemes or
612 opts.keep_going or opts.list or opts.cases or opts.verbose or
613 len(args)>0):
614 parser.error(
615 'Option --clean may not be used with arguments and other options')
616 if opts.default_schemes and not opts.testmode:
617 parser.error(
618 'Option --default-schemes can only be used with --test')
619 if len(args) > 1:
620 parser.error(
621 'May not be called with more than one argument')
623 if opts.list:
624 echo('\n'.join(self.steps()))
625 return 0
626 if opts.cases:
627 echo('\n'.join(self.cases()))
628 return 0
629 if opts.clean:
630 self.clean()
631 return 0
633 self.default_schemes = opts.default_schemes
634 self.keep_going = opts.keep_going
635 self.test_mode = opts.testmode
636 self.verbose = opts.verbose
637 self.did_run = True
638 step = None
639 if len(args):
640 step = args[0]
641 return int(not self.run(step))
643 class RunApp(object):
644 """Functor to run a FreeFOAM application."""
645 def __init__(self, app, parallel=False, args=[]):
646 """Creates a new application runner.
648 Parameters
649 ----------
650 app : Name or full/relative path to the application to run.
651 parallel : If True, the '-parallel' flag will be passed to the
652 application.
653 args : Additional arguments and options to be passed to the
654 application.
657 self._app = app
658 self._parallel = parallel
659 self._args = args
660 self._runner = _fr.ParallelRunner()
662 def __call__(self, case_dir, stamp_file, testmode):
663 """Runs the FreeFOAM application.
665 The function redirects the output to `self.logfile`. Before running
666 the application, it writes a short header to the log, listing the
667 application name, the case directory and the command line used to
668 invoke the application. At the end of the run it writes a `REPORT:
669 SUCCESS` if the application returned 0, or `REPORT: FAILURE` for all
670 other return values or if an exception was caught.
672 Returns
673 -------
674 True on success, False otherwise.
677 import sys
678 import subprocess
679 returncode = 1
680 stamp_file.write(''.join([80*'='+'\nAPPLICATION: ', self._app, '\n']))
681 stamp_file.write(''.join(['CASE: ', case_dir, '\n']))
682 stamp_file.write(''.join(['COMMAND LINE: ', self._runner.command_str(
683 self._app, case=case_dir, parallel=self._parallel,
684 args=self._args), '\n'+80*'='+'\n\n']))
685 stamp_file.flush()
686 try:
687 # this is ridiculous, but python 2.4 doesn't support try-except-finally
688 try:
689 if testmode:
690 _os.environ['FREEFOAM_CONFIG_DIR'] = _op.join(case_dir, 'etc')
691 self._runner.run(self._app, case=case_dir,
692 parallel=self._parallel, stdout=subprocess.PIPE,
693 stderr=subprocess.STDOUT, args=self._args, background=True) == 0
694 # retrieve the output and write it to the log file as long as the thing runs
695 while True:
696 lines = self._runner.getProcess().stdout.readlines()
697 if len(lines) and not isinstance(lines[0], str):
698 lines = list(map(lambda l: l.decode(), lines))
699 stamp_file.writelines(lines)
700 stat = self._runner.poll()
701 if stat != None:
702 break
703 stat = stat == 0
704 except KeyboardInterrupt:
705 stamp_file.write('\n*** Error *** KeyboardInterrupt\n')
706 stat = False
707 raise
708 except Exception, e:
709 stamp_file.write('\n*** Error *** '+str(e)+'\n')
710 stat = False
711 raise
712 finally:
713 # write final verdict to the log and clean up
714 stamp_file.write('\n'+80*'='+'\nREPORT: ')
715 if stat:
716 stamp_file.write('SUCCESS\n')
717 else:
718 stamp_file.write('FAILURE\n')
719 return stat
721 class CompileApp(object):
722 """Functor to compile a FreeFOAM tutorial application.
724 Attributes
725 ----------
726 srcdir : The top-level source directory.
727 bindir : The top-level binary directory.
728 app : Absolute path to the binary application.
731 def __init__(self, srcdir, bindir=None, name=None):
732 """Initializes a new CompileApp instance.
734 Parameters
735 ----------
736 srcdir : The top-level source directory.
737 bindir : The top-level binary directory (i.e. where to build the
738 application). If set to None, defaults to 'srcdir+"-build"'.
739 name : Name of the application. Defaults to `os.path.basename(srcdir)`.
742 self.srcdir = _op.abspath(srcdir)
743 if bindir == None:
744 bindir = self.srcdir+'-build'
745 if name == None:
746 name = _op.basename(self.srcdir)
747 self.bindir = bindir
748 self.app = _op.join(self.bindir, _f.EXE_PREFIX+name+_f.EXE_SUFFIX)
750 def __call__(self, case_dir, stamp_file, testmode):
751 """Compile a FreeFOAM application.
753 Configures and compiles a FreeFOAM tutorial application located in the
754 source directory `self.srcdir`.
757 import subprocess
758 # get the current working directory
759 pwd = _os.getcwd()
760 # create the binary directory and cd to it
761 if not _op.exists(self.bindir):
762 _os.makedirs(self.bindir)
763 elif not _op.isdir(self.bindir):
764 raise BinDirCreationError(self.bindir,
765 'The path exists, but is not a directory')
766 returncode = 1
767 msg = None
768 _os.chdir(self.bindir)
769 # configure the project
770 cmd = [
771 _f.CMAKE_COMMAND,
772 '-DCMAKE_C_COMPILER='+_f.CMAKE_C_COMPILER,
773 '-DCMAKE_CXX_COMPILER='+_f.CMAKE_CXX_COMPILER,
774 '-DCMAKE_BUILD_TYPE='+compile_config,
775 '-G', _f.CMAKE_GENERATOR,
776 self.srcdir
778 stamp_file.write(''.join(['CONFIGURING: ', ' '.join(cmd),
779 '\n'+80*'='+'\n\n']))
780 stamp_file.flush()
781 try:
782 process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
783 stderr=subprocess.STDOUT)
784 while True:
785 lines = process.stdout.readlines()
786 if len(lines) and not isinstance(lines[0], str):
787 lines = list(map(lambda l: l.decode(), lines))
788 stamp_file.writelines(lines)
789 returncode = process.poll()
790 if returncode != None:
791 break
792 stamp_file.write('\n'+80*'='+'\nREPORT: ')
793 if returncode:
794 msg = 'Failed to configure the project "%s" in "%s".'%(
795 self.srcdir, self.bindir)
796 stamp_file.write('FAILURE\n')
797 return False
798 else:
799 stamp_file.write('SUCCESS\n')
800 # kick off the build
801 cmd = [_f.CMAKE_COMMAND, '--build', '.',
802 '--config', compile_config]
803 stamp_file.write(''.join(['COMPILING: ', ' '.join(cmd),
804 '\n'+80*'='+'\n\n']))
805 stamp_file.flush()
806 returncode = subprocess.call(
807 cmd, stdout=stamp_file, stderr=stamp_file)
808 stamp_file.write('\n'+80*'='+'\nREPORT: ')
809 if returncode:
810 msg = 'Failed to build the application "%s" in "%s".'%(
811 self.srcdir, self.bindir)
812 stamp_file.write('FAILURE\n')
813 return False
814 else:
815 stamp_file.write('SUCCESS\n')
816 finally:
817 _os.chdir(pwd)
818 return True
821 def clean_application(bindir):
822 """Cleans a FreeFOAM application.
824 Cleans a FreeFOAM tutorial application located in the binary directory
825 `bindir`.
827 Parameters
828 ----------
829 bindir : The top-level binary directory (i.e. where the build directory is.
832 import subprocess
833 bindir = _op.normpath(bindir)
834 returncode = 0
835 if _op.isdir(bindir):
836 cmd = [_f.CMAKE_COMMAND, '--build', bindir, '--target', 'clean']
837 returncode = subprocess.call(cmd)
838 if returncode:
839 raise CleaningFailedError(bindir)
840 return
842 def modify_file(fname, regSub, backup=None):
843 """Modifies file `fname` by applying the regular expression/substitution
844 pairs in the iterable `regSub`. If `backup` is a string and not None, a
845 backup file with the given suffix will be created before the modification
846 takes place."""
847 import re
848 fname = _op.abspath(fname)
849 if backup and type(backup) == str:
850 import shutil
851 shutil.copy2(fname, fname+backup)
852 lines=open(fname, 'rt').readlines()
853 for i, l in enumerate(lines):
854 for p, s in regSub:
855 if re.search(p, l):
856 lines[i] = re.sub(p, s, l)
857 open(fname, 'wt').writelines(lines)
859 # ------------------------- vim: set sw=3 sts=3 et: --------------- end-of-file