util: Add a README file for the m5 utility.
[gem5.git] / util / build_cross_gcc / build_cross_gcc.py
1 #! /usr/bin/env python
2 # Copyright 2020 Google, Inc.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met: redistributions of source code must retain the above copyright
7 # notice, this list of conditions and the following disclaimer;
8 # redistributions in binary form must reproduce the above copyright
9 # notice, this list of conditions and the following disclaimer in the
10 # documentation and/or other materials provided with the distribution;
11 # neither the name of the copyright holders nor the names of its
12 # contributors may be used to endorse or promote products derived from
13 # this software without specific prior written permission.
14 #
15 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27 import abc
28 import argparse
29 import glob
30 import multiprocessing
31 import os
32 import os.path
33 import pickle
34 import shutil
35 import six
36 import subprocess
37 import textwrap
38
39 SETTINGS_FILE = '.build_cross_gcc.settings'
40 LOG_FILE = 'build_cross_gcc.log'
41
42 all_settings = {}
43 all_steps = {}
44
45 description_paragraphs = [
46 '''
47 This script helps automate building a gcc based cross compiler.
48 The process is broken down into a series of steps which can be
49 executed one at a time or in arbtitrary sequences. It's assumed that
50 you've already downloaded the following sources into the current
51 directory:''',
52 '',
53 '''1. binutils''',
54 '''2. gcc''',
55 '''3. glibc''',
56 '''4. linux kernel''',
57 '''5. gdb''',
58 '',
59 '''
60 The entire process can be configured with a series of settings
61 which are stored in a config file called {settings_file}. These
62 settings can generally also be set from the command line, and at run
63 time using step 0 of the process. Many will set themselves to
64 reasonable defaults if no value was loaded from a previous
65 configuration or a saved settings file.''',
66 '',
67 '''
68 Prebaked config options can be loaded in from an external file to
69 make it easier to build particular cross compilers without having to
70 mess with a lot of options.'''
71 '',
72 '''
73 When settings are listed, any setting which has a value which has
74 failed validation or which hasn't been set and doesn't have a
75 reasonable default will be marked with a X in the far left hand
76 column. Settings will generally refuse to be set to invalid values,
77 unless they were like that by default and the user refused to correct
78 them.''',
79 '',
80 '''This script is based on the excellent how-to here:''',
81 '''https://preshing.com/20141119/how-to-build-a-gcc-cross-compiler/''',
82 '',
83 '''
84 Please view that webpage for a detailed explanation of what this
85 script does.'''
86 ]
87
88 def help_text_wrapper(text):
89 width = shutil.get_terminal_size().columns
90 text = textwrap.dedent(text)
91 text = text.strip()
92 return textwrap.fill(text, width=width)
93
94 description = '\n'.join(list(map(help_text_wrapper, description_paragraphs)))
95
96 argparser = argparse.ArgumentParser(
97 formatter_class=argparse.RawDescriptionHelpFormatter,
98 description=description)
99
100
101 #
102 # Some helper utilities.
103 #
104
105 def confirm(prompt):
106 while True:
107 yn = input('{} (N/y): '.format(prompt))
108 if yn == '':
109 yn = 'n'
110 if yn.lower() in ('y', 'Yes'):
111 return True
112 elif yn.lower() in ('n', 'No'):
113 return False
114
115
116 def setup_build_dir(subdir):
117 build_dir_base = BuildDirBase.setting()
118 target = Target.setting()
119 if not (build_dir_base.valid and target.valid):
120 return False
121 target_build_dir = os.path.join(build_dir_base.get(), target.get())
122 build_dir = os.path.join(target_build_dir, 'build-{}'.format(subdir))
123 if not os.path.isdir(build_dir):
124 os.makedirs(build_dir)
125 return build_dir
126
127 def run_commands(working_dir, *cmds):
128 with open(LOG_FILE, 'a') as log:
129 print('In working directory {:s} (log in {:s}):'.format(
130 working_dir, LOG_FILE))
131 for cmd in cmds:
132 print(textwrap.fill(cmd, initial_indent=' ',
133 subsequent_indent=' ',
134 width=shutil.get_terminal_size().columns))
135 print('', file=log)
136 print(cmd, file=log)
137 print('', file=log)
138 if subprocess.call(cmd, shell=True, cwd=working_dir,
139 stdout=log, stderr=subprocess.STDOUT) != 0:
140 return False
141 return True
142
143
144 #
145 # Settings.
146 #
147
148 class MetaSetting(type):
149 def __new__(mcls, name, bases, d):
150 cls = super(MetaSetting, mcls).__new__(mcls, name, bases, d)
151 key = d.get('key', None)
152 if key is not None:
153 assert('default' in d)
154 instance = cls()
155 instance.value = None
156 instance.valid = False
157 all_settings[key] = instance
158 return cls
159
160 @six.add_metaclass(MetaSetting)
161 @six.add_metaclass(abc.ABCMeta)
162 class Setting(object):
163 key = None
164
165 @abc.abstractmethod
166 def set(self, value):
167 'Validate and set the setting to "value", and return if successful.'
168 self.value = value
169 self.valid = True
170 return True
171
172 def set_default(self):
173 'Set this setting to its default value, and return if successful.'
174 return self.set(self.default)
175
176 def set_arg(self, value):
177 'Set this setting to value if not None, and return if successful.'
178 if value:
179 return self.set(value)
180 else:
181 # Nothing happened, so nothing failed.
182 return True
183
184 def get(self):
185 'Return the value of this setting.'
186 return self.value
187
188 @abc.abstractmethod
189 def describe(self):
190 'Return a string describing this setting.'
191 return ''
192
193 @abc.abstractmethod
194 def add_to_argparser(self, argparser):
195 'Add command line options associated with this setting.'
196
197 @abc.abstractmethod
198 def set_from_args(self, args):
199 'Set this setting from the command line arguments, if requested.'
200 return True
201
202 @classmethod
203 def setting(cls):
204 s = all_settings[cls.key]
205 if not s.valid:
206 print('"{}" is not valid.'.format(s.key))
207 return s
208
209 class DirectorySetting(Setting):
210 def set(self, value):
211 if not os.path.exists(value):
212 print('Path "{:s}" does not exist.'.format(value))
213 elif not os.path.isdir(value):
214 print('Path "{:s}" is not a directory.'.format(value))
215 else:
216 self.value = value
217 self.valid = True
218 return self.valid
219
220 def set_default(self):
221 if not self.set(self.default):
222 if not os.path.exists(self.default):
223 if confirm('Create?'):
224 try:
225 os.mkdirs(value)
226 assert(self.set(self.default))
227 except:
228 print('Failed to make directory')
229 self.valid = False
230 return False
231 else:
232 self.value = self.default
233 self.valid = False
234 return False
235
236 class Prefix(DirectorySetting):
237 default = os.path.join(os.environ['HOME'], 'cross')
238 key = 'PREFIX'
239
240 def describe(self):
241 return 'Path prefix to install to.'
242
243 def add_to_argparser(self, parser):
244 parser.add_argument('--prefix', help=self.describe())
245
246 def set_from_args(self, args):
247 return self.set_arg(args.prefix)
248
249 class BuildDirBase(DirectorySetting):
250 default = os.getcwd()
251 key = 'BUILD_DIR_BASE'
252
253 def describe(self):
254 return 'Path prefix for build directory(ies).'
255
256 def add_to_argparser(self, parser):
257 parser.add_argument('--build-dir-base', help=self.describe())
258
259 def set_from_args(self, args):
260 return self.set_arg(args.build_dir_base)
261
262 class Target(Setting):
263 key = 'TARGET'
264 default = None
265
266 def set_default(self):
267 self.value = '(not set)'
268 self.valid = False
269 return False
270
271 def describe(self):
272 return 'Tuple for the target architecture.'
273
274 def add_to_argparser(self, parser):
275 parser.add_argument('--target', help=self.describe())
276
277 def set_from_args(self, args):
278 return self.set_arg(args.target)
279
280 class LinuxArch(Setting):
281 key = 'LINUX_ARCH'
282 default = None
283
284 def set_default(self):
285 self.value = '(not set)'
286 self.valid = False
287 return False
288
289 def describe(self):
290 return 'The arch directory for Linux headers.'
291
292 def add_to_argparser(self, parser):
293 parser.add_argument('--linux-arch', help=self.describe())
294
295 def set_from_args(self, args):
296 return self.set_arg(args.linux_arch)
297
298 class SourceDirSetting(Setting):
299 def set(self, value):
300 if os.path.isdir(value):
301 self.value = value
302 self.valid = True
303 return self.valid
304
305 def set_default(self):
306 matches = list(filter(os.path.isdir, glob.glob(self.pattern)))
307 if len(matches) == 0:
308 self.valid = False
309 return False
310 if len(matches) > 1:
311 while True:
312 print()
313 print('Multple options for "{:s}":'.format(self.key))
314 choices = list(enumerate(matches))
315 for number, value in choices:
316 print('{:>5}: {:s}'.format(number, value))
317 choice = input('Which one? ')
318 try:
319 choice = choices[int(choice)][1]
320 except:
321 print('Don\'t know what to do with "{:s}".'.format(choice))
322 continue
323 return self.set(choice)
324 return self.set(matches[0])
325
326 def describe(self):
327 return 'Directory with the extracted {} source.'.format(self.project)
328
329 class BinutilsSourceDir(SourceDirSetting):
330 key = 'BINUTILS_SRC_DIR'
331 default = None
332 pattern = 'binutils-*'
333 project = 'binutils'
334
335 def add_to_argparser(self, parser):
336 parser.add_argument('--binutils-src', help=self.describe())
337
338 def set_from_args(self, args):
339 return self.set_arg(args.binutils_src)
340
341 class GccSourceDir(SourceDirSetting):
342 key = 'GCC_SRC_DIR'
343 default = None
344 pattern = 'gcc-*'
345 project = 'gcc'
346
347 def add_to_argparser(self, parser):
348 parser.add_argument('--gcc-src', help=self.describe())
349
350 def set_from_args(self, args):
351 return self.set_arg(args.gcc_src)
352
353 class GlibcSourceDir(SourceDirSetting):
354 key = 'GLIBC_SRC_DIR'
355 default = None
356 pattern = 'glibc-*'
357 project = 'glibc'
358
359 def add_to_argparser(self, parser):
360 parser.add_argument('--glibc-src', help=self.describe())
361
362 def set_from_args(self, args):
363 return self.set_arg(args.glibc_src)
364
365 class LinuxSourceDir(SourceDirSetting):
366 key = 'LINUX_SRC_DIR'
367 default = None
368 pattern = 'linux-*'
369 project = 'linux'
370
371 def add_to_argparser(self, parser):
372 parser.add_argument('--linux-src', help=self.describe())
373
374 def set_from_args(self, args):
375 return self.set_arg(args.linux_src)
376
377 class GdbSourceDir(SourceDirSetting):
378 key = 'GDB_SRC_DIR'
379 default = None
380 pattern = 'gdb-*'
381 project = 'gdb'
382
383 def add_to_argparser(self, parser):
384 parser.add_argument('--gdb-src', help=self.describe())
385
386 def set_from_args(self, args):
387 return self.set_arg(args.gdb_src)
388
389 class Parallelism(Setting):
390 key = 'J'
391 default = None
392
393 def set(self, value):
394 try:
395 value = int(value)
396 except:
397 print('Can\'t convert "{:s}" into an integer.'.format(value))
398 if value < 0:
399 print('Parallelism can\'t be negative.')
400 return False
401 self.value = value
402 self.valid = True
403 return self.valid
404
405 def set_default(self):
406 self.set(multiprocessing.cpu_count())
407
408 def describe(self):
409 return 'The level of parellism to request from "make".'
410
411 def add_to_argparser(self, parser):
412 parser.add_argument('-j', help=self.describe())
413
414 def set_from_args(self, args):
415 return self.set_arg(args.j)
416
417
418
419 #
420 # Steps of the build process.
421 #
422
423 class MetaStep(type):
424 def __new__(mcls, name, bases, d):
425 cls = super(MetaStep, mcls).__new__(mcls, name, bases, d)
426 number = d.get('number', None)
427 if number is not None:
428 all_steps[number] = cls()
429 return cls
430
431 @six.add_metaclass(MetaStep)
432 @six.add_metaclass(abc.ABCMeta)
433 class Step(object):
434 'Steps to set up a cross compiling gcc.'
435 number = None
436
437 @abc.abstractmethod
438 def run(self):
439 'Execute this step.'
440 pass
441
442 @abc.abstractmethod
443 def describe(self):
444 'Return a string describing this step.'
445 return ''
446
447
448 class Configure(Step):
449 number = 0
450
451 def describe(self):
452 return 'Adjust settings.'
453
454 def get_setting(self):
455 settings = list(enumerate(all_settings.items()))
456 all_keys = list(all_settings.keys())
457 max_key_length = max([len(key) for key in all_keys])
458 while True:
459 for number, (key, setting) in settings:
460 print('{}{:>4}: {:{key_len}s} - {:s}'.format(
461 ' ' if setting.valid else 'X',
462 number, key, setting.describe(), key_len=max_key_length))
463 print(' {}'.format(setting.value))
464 print()
465 key = input('Value to modify, or "done": ')
466 if key == "done":
467 save_settings()
468 return None
469 if key not in all_keys:
470 try:
471 key = settings[int(key)][1][0]
472 except:
473 print('Don\'t know what to do with "{:s}."'.format(key))
474 continue
475 return all_settings[key]
476
477 def run(self):
478 while True:
479 setting = self.get_setting()
480 if not setting:
481 return True
482
483 new_value = input('New value ({:s}): '.format(setting.get()))
484 if new_value:
485 setting.set(new_value)
486 save_settings()
487
488 print_settings()
489 return True
490
491 class BuildBinutils(Step):
492 number = 1
493
494 def describe(self):
495 return 'Build binutils.'
496
497 def run(self):
498 prefix = Prefix.setting()
499 target = Target.setting()
500 j = Parallelism.setting()
501 source_dir = BinutilsSourceDir.setting()
502 build_dir = setup_build_dir('binutils')
503
504 if not all((prefix, target, j, source_dir, build_dir)):
505 return False
506
507 prefix = prefix.get()
508 target = target.get()
509 j = j.get()
510 build_dir = os.path.abspath(build_dir)
511 source_dir = os.path.abspath(source_dir.get())
512
513 return run_commands(build_dir,
514 '{configure} --prefix={prefix} --target={target} '
515 '--disable-multilib'.format(
516 configure=os.path.join(source_dir, 'configure'),
517 prefix=prefix, target=target),
518 'make -j{j}'.format(j=j),
519 'make install'
520 )
521
522 class InstallLinuxHeaders(Step):
523 number = 2
524
525 def describe(self):
526 return 'Install Linux headers.'
527
528 def run(self):
529 source_dir = LinuxSourceDir.setting()
530 linux_arch = LinuxArch.setting()
531 prefix = Prefix.setting()
532 target = Target.setting()
533
534 if not all((source_dir, linux_arch, prefix, target)):
535 return False
536
537 source_dir = os.path.abspath(source_dir.get())
538 linux_arch = linux_arch.get()
539 prefix = os.path.abspath(prefix.get())
540 target = target.get()
541
542 hdr_path = os.path.join(prefix, target)
543
544 return run_commands(source_dir,
545 'make ARCH={arch} INSTALL_HDR_PATH={hdr_path} '
546 'headers_install'.format(arch=linux_arch, hdr_path=hdr_path))
547
548 class Compilers(Step):
549 number = 3
550
551 def describe(self):
552 return 'Build C and C++ compilers.'
553
554 def run(self):
555 prefix = Prefix.setting()
556 target = Target.setting()
557 j = Parallelism.setting()
558 source_dir = GccSourceDir.setting()
559 build_dir = setup_build_dir('gcc')
560
561 if not all((prefix, target, j, source_dir, build_dir)):
562 return False
563
564 prefix = prefix.get()
565 target = target.get()
566 j = j.get()
567 build_dir = os.path.abspath(build_dir)
568 source_dir = os.path.abspath(source_dir.get())
569
570 return run_commands(build_dir,
571 '{configure} --prefix={prefix} --target={target} '
572 '--enable-languages=c,c++ --disable-multilib'.format(
573 configure=os.path.join(source_dir, 'configure'),
574 prefix=prefix, target=target),
575 'make -j{j} all-gcc LIMITS_H_TEST=true'.format(j=j),
576 'make install-gcc'
577 )
578
579 class CHeaders(Step):
580 number = 4
581
582 def describe(self):
583 return 'Standard C library headers and startup files.'
584
585 def run(self):
586 prefix = Prefix.setting()
587 target = Target.setting()
588 j = Parallelism.setting()
589 source_dir = GlibcSourceDir.setting()
590 build_dir = setup_build_dir('glibc')
591
592 if not all((prefix, target, j, source_dir, build_dir)):
593 return False
594
595 prefix = prefix.get()
596 target = target.get()
597 j = j.get()
598 source_dir = os.path.abspath(source_dir.get())
599 build_dir = os.path.abspath(build_dir)
600
601 return run_commands(build_dir,
602 '{configure} --prefix={prefix} --build=$MACHTYPE '
603 '--host={host} --target={target} --with-headers={hdr_path} '
604 '--disable-multilib libc_cv_forced_unwind=yes'.format(
605 configure=os.path.join(source_dir, 'configure'),
606 prefix=os.path.join(prefix, target),
607 host=target, target=target,
608 hdr_path=os.path.join(prefix, target, 'include')),
609 'make install-bootstrap-headers=yes install-headers',
610 'make -j{j} csu/subdir_lib'.format(j=j),
611 'install csu/crt1.o csu/crti.o csu/crtn.o {lib_path}'.format(
612 lib_path=os.path.join(prefix, target, 'lib')),
613 '{target}-gcc -nostdlib -nostartfiles -shared -x c /dev/null '
614 '-o {libc_so}'.format(target=target,
615 libc_so=os.path.join(prefix, target, 'lib', 'libc.so')),
616 'touch {stubs_h}'.format(stubs_h=os.path.join(
617 prefix, target, 'include', 'gnu', 'stubs.h'))
618 )
619
620 class CompilerSupportLib(Step):
621 number = 5
622
623 def describe(self):
624 return 'Build the compiler support library.'
625
626 def run(self):
627 j = Parallelism.setting()
628 build_dir = setup_build_dir('gcc')
629
630 if not all((j, build_dir)):
631 return False
632
633 j = j.get()
634 build_dir = os.path.abspath(build_dir)
635
636 return run_commands(build_dir,
637 'make -j{j} all-target-libgcc'.format(j=j),
638 'make install-target-libgcc'
639 )
640
641 class StandardCLib(Step):
642 number = 6
643
644 def describe(self):
645 return 'Install the standard C library.'
646
647 def run(self):
648 j = Parallelism.setting()
649 build_dir = setup_build_dir('glibc')
650
651 if not all((j, build_dir)):
652 return False
653
654 j = j.get()
655 build_dir = os.path.abspath(build_dir)
656
657 return run_commands(build_dir,
658 'make -j{j}'.format(j=j),
659 'make install',
660 )
661
662 class BuildGdb(Step):
663 number = 7
664
665 def describe(self):
666 return 'Build GDB.'
667
668 def run(self):
669 prefix = Prefix.setting()
670 target = Target.setting()
671 j = Parallelism.setting()
672 source_dir = GdbSourceDir.setting()
673 build_dir = setup_build_dir('gdb')
674
675 if not all((prefix, target, j, source_dir, build_dir)):
676 return False
677
678 prefix = prefix.get()
679 target = target.get()
680 j = j.get()
681 source_dir = os.path.abspath(source_dir.get())
682 build_dir = os.path.abspath(build_dir)
683
684 return run_commands(build_dir,
685 '{configure} --prefix={prefix} --target={target} '
686 '$MACHTYPE'.format(prefix=prefix, target=target,
687 configure=os.path.join(source_dir, 'configure')),
688 'make -j{j}'.format(j=j),
689 'make install'
690 )
691
692 class StandardCxxLib(Step):
693 number = 8
694
695 def describe(self):
696 return 'Install the standard C++ library.'
697
698 def run(self):
699 j = Parallelism.setting()
700 build_dir = setup_build_dir('gcc')
701
702 if not all((j, build_dir)):
703 return False
704
705 j = j.get()
706 build_dir = os.path.abspath(build_dir)
707
708 return run_commands(build_dir,
709 'make -j{j}'.format(j=j),
710 'make install'
711 )
712
713
714 #
715 # The engine that makes it all go.
716 #
717
718 def get_steps():
719 while True:
720 print()
721 print('Steps:')
722 for _, step in sorted(all_steps.items()):
723 print('{:>5} {:s}'.format(
724 '{:d}:'.format(step.number), step.describe()))
725 print()
726 steps = input('Comma separated list of steps, or '
727 '"exit", or "all" (all): ')
728 if not steps:
729 steps = 'all'
730 if steps == 'exit':
731 return []
732 if steps == 'all':
733 keys = list([str(key) for key in all_steps.keys()])
734 steps = ','.join(keys)
735 try:
736 return list([all_steps[int(i)] for i in steps.split(",")])
737 except:
738 print('Don\'t know what to do with "{:s}"'.format(steps))
739
740 def print_settings():
741 print()
742 print('Settings:')
743 for setting in all_settings.values():
744 print('{} {} = {}'.format(
745 ' ' if setting.valid else 'X', setting.key, setting.value))
746
747 def save_settings():
748 settings = {}
749 for setting in all_settings.values():
750 if setting.valid:
751 settings[setting.key] = setting.get()
752 with open(SETTINGS_FILE, 'wb') as settings_file:
753 pickle.dump(settings, settings_file)
754
755 def load_settings():
756 if os.path.exists(SETTINGS_FILE):
757 with open(SETTINGS_FILE, 'rb') as settings_file:
758 settings = pickle.load(settings_file)
759 else:
760 settings = {}
761
762 for setting in all_settings.values():
763 if setting.key in settings:
764 setting.set(settings[setting.key])
765
766 def load_settings_file(path):
767 with open(path, 'r') as settings:
768 for line in settings.readlines():
769 if not line:
770 continue
771 try:
772 key, val = line.split('=')
773 except:
774 print('Malformated line "{}" in settings file "{}".'.format(
775 line, path))
776 return False
777 key = key.strip()
778 val = val.strip()
779 if key not in all_settings:
780 print('Unknown setting "{}" found in settings '
781 'file "{}".'.format(key, path))
782 return False
783 setting = all_settings[key]
784 if not setting.set(val):
785 print('Failed to set "{}" to "{}" from '
786 'settings file "{}".'.format(key, val, path))
787 return False
788 return True
789
790
791
792 argparser.add_argument('--settings-file',
793 help='A file with name=value settings to load.')
794
795 def main():
796 # Install command line options for each setting.
797 for setting in all_settings.values():
798 setting.add_to_argparser(argparser)
799
800 args = argparser.parse_args()
801
802 # Load settings from the last time we ran. Lowest priority.
803 load_settings()
804
805 # If requested, read in a settings file. Medium priority.
806 if args.settings_file:
807 if not load_settings_file(args.settings_file):
808 return
809
810 # Set settings based on command line options. Highest priority.
811 for setting in all_settings.values():
812 setting.set_from_args(args)
813
814 # If a setting is still not valid, try setting it to its default.
815 for setting in all_settings.values():
816 if not setting.valid:
817 setting.set_default()
818
819 # Print out the resulting settings.
820 print_settings()
821
822 while True:
823 steps = get_steps()
824 if not steps:
825 return
826 for step in steps:
827 print()
828 print('Step {:d}: {:s}'.format(step.number, step.describe()))
829 print()
830 if not step.run():
831 print()
832 print('Step failed, aborting.')
833 break
834
835 if __name__ == "__main__":
836 main()