Beispiel #1
0
    def launch_list(s, help_, verbose, all_):

        # Help message

        def print_help():
            print()
            print(bold('Usage:'), 'mflowgen stash list [--verbose] [--all]')
            print()
            print('Lists all pre-built steps stored in the mflowgen stash')
            print('that the current build graph is linked to. The --verbose')
            print('flag prints metadata about where each stashed step was')
            print('stashed from. Use --all to print all steps in the stash.')
            print()

        if help_:
            print_help()
            return

        # Sanity-check the stash

        s.verify_stash()

        # Print the list

        print()
        print(bold('Stash List'))

        template_str = \
          ' - {hash_} [ {date} ] {author} {step} -- {msg}'

        stashed_from_template_str = \
          '     > {k:30} : {v}'

        print()
        if not s.stash:
            print(' - ( the stash is empty )')
        else:
            s.stash.reverse()  # print in reverse chronological order
            n_print = 10  # print first N items
            to_print = s.stash[:n_print] if not all_ else s.stash
            for x in to_print:
                print(
                    template_str.format(
                        hash_=yellow(x['hash']),
                        date=x['date'],
                        author=x['author'],
                        step=x['step'],
                        msg=x['msg'],
                    ))
                if verbose and 'stashed-from' in x.keys():  # stashed from
                    for k, v in x['stashed-from'].items():
                        print(stashed_from_template_str.format(k=k, v=v))
                    print()
            if not all_ and len(s.stash) > n_print:
                n_extra = len(s.stash) - n_print
                print(' - (...) see', n_extra, 'more with --all')
        print()
        print(bold('Stash:'), s.get_stash_path())
        print()
Beispiel #2
0
 def print_help():
   print()
   print( bold( 'Usage:' ), 'mflowgen stash drop --hash <hash>'     )
   print()
   print( bold( 'Example:' ), 'mflowgen stash drop --hash 3e5ab4'   )
   print()
   print( 'Removes the step with the given hash from the stash.'    )
   print()
Beispiel #3
0
 def launch_help(s):
     print()
     print(bold('Mock Commands'))
     print()
     print(bold(' - init :'), 'Initialize a mock-up for a step')
     print()
     print('Run any command with -h to see more details')
     print()
Beispiel #4
0
    def launch_run(s, design, update, backend):

        # Find the construct script (and check for --update) and save the path
        # to the construct script for future use of --update

        construct_path = s.find_construct_path(design, update)
        s.save_construct_path(construct_path)

        # Import the graph for this design

        c_dirname = os.path.dirname(construct_path)
        c_basename = os.path.splitext(os.path.basename(construct_path))[0]

        sys.path.append(c_dirname)

        try:
            construct = importlib.import_module(c_basename)
        except ModuleNotFoundError:
            print()
            print(bold('Error:'), 'Could not open construct script at',
                  '"{}"'.format(construct_path))
            print()
            sys.exit(1)

        try:
            construct.construct
        except AttributeError:
            print()
            print(bold('Error:'), 'No module named "construct" in',
                  '"{}"'.format(construct_path))
            print()
            sys.exit(1)

        # Construct the graph

        g = construct.construct()

        # Generate the build files (e.g., Makefile) for the selected backend
        # build system

        if backend == 'make':
            backend_cls = MakeBackend
        elif backend == 'ninja':
            backend_cls = NinjaBackend

        b = BuildOrchestrator(g, backend_cls)
        b.build()

        # Done

        list_target = backend + " list"
        status_target = backend + " status"

        print("Targets: run \"" + list_target + "\" and \"" + status_target +
              "\"")
        print()
Beispiel #5
0
 def print_help():
   print()
   print( bold( 'Usage:' ), 'mflowgen stash pop --hash <hash>'       )
   print()
   print( bold( 'Example:' ), 'mflowgen stash pop --hash 3e5ab4'     )
   print()
   print( 'Pulls a pre-built step from the stash and then drops it'  )
   print( 'from the stash. This command literally runs pull and'     )
   print( 'drop one after the other.'                                )
   print()
Beispiel #6
0
 def print_help():
     print()
     print(bold('Usage:'), 'mflowgen stash init',
           '--path/-p <path/to/store/dir>')
     print()
     print(bold('Example:'), 'mflowgen stash init', '-p /tmp')
     print()
     print('Creates a subdirectory in the given directory and')
     print('initializes it as an mflowgen stash.')
     print()
Beispiel #7
0
 def print_help():
   print()
   print( bold( 'Usage:' ), 'mflowgen stash link',
                              '--path/-p <path/to/stash/dir>'         )
   print()
   print( bold( 'Example:' ), 'mflowgen stash link',
                            '-p /tmp/2020-0315-mflowgen-stash-3aef14' )
   print()
   print( 'Links the current build graph to an mflowgen stash so'     )
   print( 'that all stash commands interact with that stash.'         )
   print()
Beispiel #8
0
    def find_construct_path(s, design, update):

        # Check for --update first

        if update:
            try:
                data = read_yaml('.mflowgen.yml')  # get metadata
                construct_path = data['construct']
            except Exception:
                print()
                print(bold('Error:'), 'No pre-existing build in current',
                      'directory for running --update')
                print()
                sys.exit(1)
            return construct_path

        # Search in the design directory

        if not os.path.exists(design):
            print()
            print(bold('Error:'), 'Directory not found at path',
                  '"{}"'.format(design))
            print()
            sys.exit(1)

        yaml_path = os.path.abspath(design + '/.mflowgen.yml')

        if not os.path.exists(yaml_path):
            construct_path = design + '/construct.py'
        else:

            data = read_yaml(yaml_path)

            try:
                construct_path = data['construct']
            except KeyError:
                raise KeyError(
                    'YAML file "{}" must have key "construct"'.format(
                        yaml_path))

            if not construct_path.startswith('/'):  # check if absolute path
                construct_path = design + '/' + construct_path

            construct_path = os.path.abspath(construct_path)

            if not os.path.exists(construct_path):
                raise ValueError('Construct script not found at "{}"'.format(
                    construct_path))

        return construct_path
Beispiel #9
0
 def print_help():
   print()
   print( bold( 'Usage:' ), 'mflowgen stash push',
                               '--step/-s <int> --message/-m "<str>"' )
   print()
   print( bold( 'Example:' ), 'mflowgen stash push',
                                 '--step 5 -m "foo bar"'              )
   print()
   print( 'Pushes a built step to the mflowgen stash. The given step' )
   print( 'is copied to the stash using archive mode (preserves all'  )
   print( 'permissions) while following all symlinks. Then the'       )
   print( 'stashed copy is given a hash stamp and is marked as'       )
   print( 'authored by $USER (' + author + '). An optional message'   )
   print( 'can also be attached to the push.'                         )
   print()
Beispiel #10
0
 def print_help():
   print()
   print( bold( 'Usage:' ), 'mflowgen stash list'                  )
   print()
   print( 'Lists all pre-built steps stored in the mflowgen stash' )
   print( 'that the current build graph is linked to.'             )
   print()
Beispiel #11
0
 def print_help():
     print()
     print(bold('Usage:'), 'mflowgen stash push',
           '--step/-s <int> --message/-m "<str>"', '[--all]')
     print()
     print(bold('Example:'), 'mflowgen stash push',
           '--step 5 -m "foo bar"')
     print()
     print('Pushes a built step to the mflowgen stash. The given step')
     print('is copied to the stash, preserving all permissions and')
     print('following all symlinks. By default, only the outputs of a')
     print('step are stashed, but the entire step can be stashed with')
     print('--all. The stashed copy is given a hash stamp and is')
     print('marked as authored by $USER (' + author + '). An optional')
     print('message can also be attached to each push.')
     print()
Beispiel #12
0
 def print_help():
     print()
     print(bold('Usage:'), 'mflowgen mock init',
           '--path/-p <path/to/step/dir>')
     print()
     print(bold('Example:'))
     print()
     print('  % cd mflowgen/steps')
     print('  % mkdir build && cd build')
     print('  % mflowgen mock init --path ../synopsys-dc-synthesis')
     print()
     print('Creates a mock-up graph to help develop a modular step.')
     print('The mock-up contains the "design-under-test" node and a')
     print('"push" node that connects to the inputs. You can place')
     print('inputs to your step in this node with full access to')
     print('normal build targets (e.g., make status) to make sure')
     print('your step works.')
     print()
Beispiel #13
0
 def print_help():
   print()
   print( bold( 'Usage:' ), 'mflowgen stash pull --hash <hash>'       )
   print()
   print( bold( 'Example:' ), 'mflowgen stash pull --hash 3e5ab4'     )
   print()
   print( 'Pulls a pre-built step from the stash matching the given'  )
   print( 'hash. This command copies the pre-built step from the'     )
   print( 'stash using archive mode (preserves all permissions). The' )
   print( 'new step replaces the same step in the existing graph.'    )
   print( 'For example if the existing graph marks'                   )
   print( '"synopsys-dc-synthesis" as step 4 and a pre-built'         )
   print( '"synopsys-dc-synthesis" is pulled from the stash, the'     )
   print( 'existing build directory is removed and the pre-built'     )
   print( 'version replaces it as step 4. The status of the'          )
   print( 'pre-built step is forced to be up to date until the step'  )
   print( 'is cleaned.'                                               )
   print()
Beispiel #14
0
 def print_help():
     print()
     print(bold('Usage:'), 'mflowgen stash list [--verbose] [--all]')
     print()
     print('Lists all pre-built steps stored in the mflowgen stash')
     print('that the current build graph is linked to. The --verbose')
     print('flag prints metadata about where each stashed step was')
     print('stashed from. Use --all to print all steps in the stash.')
     print()
Beispiel #15
0
    def launch_list(s, help_):

        # Help message

        def print_help():
            print()
            print(bold('Usage:'), 'mflowgen stash list')
            print()
            print('Lists all pre-built steps stored in the mflowgen stash')
            print('that the current build graph is linked to.')
            print()

        if help_:
            print_help()
            return

        # Sanity-check the stash

        s.verify_stash()

        # Print the list

        print()
        print(bold('Stash List'))

        template_str = \
          ' - {hash_} [ {date} ] {author} {step} -- {msg}'

        print()
        if not s.stash:
            print(' - ( the stash is empty )')
        else:
            for x in s.stash:
                print(
                    template_str.format(
                        hash_=yellow(x['hash']),
                        date=x['date'],
                        author=x['author'],
                        step=x['step'],
                        msg=x['msg'],
                    ))
        print()
        print(bold('Stash:'), s.get_stash_path())
        print()
Beispiel #16
0
 def get_hash_index_in_stash( s, hash_ ):
   is_target = [ hash_ == data[ 'hash' ] for data in s.stash ]
   try:
     assert any( is_target )
   except AssertionError:
     print( bold( 'Error:' ), 'Stash does not contain hash',
                                 '"{}"'.format( hash_ ) )
     sys.exit( 1 )
   ind  = is_target.index( True )
   return ind
Beispiel #17
0
 def verify_stash(s):
     try:
         assert os.path.exists(s.link_path)
     except AssertionError:
         # Stash not found... print a useful message with directions
         stash_msg = s.link_path if s.link_path else '(no stash is linked)'
         print()
         print(
             'Could not access stash directory.',
             'Please continue with one of the following:\n', '\n',
             '1. Create a new stash (with stash init)\n',
             '2. Relink to a new stash (with stash link)\n',
             '3. Make sure the currently linked stash directory exists\n',
             '\n', bold('Stash:'), '{}'.format(stash_msg))
         print()
         sys.exit(1)
Beispiel #18
0
 def launch_help( s ):
   print()
   print( bold( 'Stash Commands' ) )
   print()
   print( bold( ' - init :' ), 'Initialize a stash'                                )
   print( bold( ' - link :' ), 'Link the current build graph to an existing stash' )
   print()
   print( bold( ' - list :' ), 'List all pre-built steps in the stash'             )
   print()
   print( bold( ' - push :' ), 'Push a built step to the stash'                    )
   print( bold( ' - pull :' ), 'Pull a built step from the stash'                  )
   print( bold( ' - drop :' ), 'Remove a built step from the stash'                )
   print()
   print( 'Run any command with -h to see more details'                 )
   print()
Beispiel #19
0
  def launch_drop( s, help_, hash_ ):

    # Help message

    def print_help():
      print()
      print( bold( 'Usage:' ), 'mflowgen stash drop --hash <hash>'     )
      print()
      print( bold( 'Example:' ), 'mflowgen stash drop --hash 3e5ab4'   )
      print()
      print( 'Removes the step with the given hash from the stash.'    )
      print()

    if help_ or not hash_:
      print_help()
      return

    # Sanity-check the stash

    s.verify_stash()

    # Get the step metadata

    ind  = s.get_hash_index_in_stash( hash_ )
    data = s.stash[ ind ]

    # Now delete the target from the stash

    remote_path = s.get_stash_path() + '/' + data[ 'dir' ]

    try:
      shutil.rmtree( remote_path )
    except Exception as e:
      print( bold( 'Error:' ), 'Failed to complete stash drop' )
      raise

    # Update the metadata in the stash

    del( s.stash[ ind ] )
    s.update_stash()

    print(
      'Dropped step "{step_name}" with hash "{hash_}"'.format(
      step_name = data[ 'step' ],
      hash_     = hash_,
    ) )
Beispiel #20
0
  def launch( s ):

    try:
      os.makedirs( 'mflowgen-demo' )
    except OSError:
      if not os.path.isdir( 'mflowgen-demo' ):
        raise

    try:
      shutil.copytree( src      = s.demo_src_path,
                       dst      = s.demo_dst_path,
                       symlinks = False,
                       ignore_dangling_symlinks = False )
    except FileExistsError:
      pass
    except Exception as e:
      print( bold( 'Error:' ), 'Could not copy demo from install' )
      raise

    print()
    print( bold( 'Demo Circuit for mflowgen -- Greatest Common Divisor' ))
    print()
    print( 'A demo design has been provided for you in "mflowgen-demo"' )
    print( 'of a simple arithmetic circuit with some state. To get'     )
    print( 'started, run the following commands:'                       )
    print()
    print( bold( '  %' ), 'cd mflowgen-demo' )
    print( bold( '  %' ), 'mkdir build && cd build' )
    print( bold( '  %' ), 'mflowgen run --design ../GcdUnit' )
    print()
    print( bold( '  %' ), 'make list     # See all steps' )
    print( bold( '  %' ), 'make status   # See build status' )
    print()
    print( 'You can also generate a PDF of the graph with graphviz.' )
    print()
    print( bold( '  %' ), 'make graph' )
    print( '   (open graph.pdf)' )
    print()
Beispiel #21
0
    def launch_init(s, help_, path):

        # Help message

        def print_help():
            print()
            print(bold('Usage:'), 'mflowgen mock init',
                  '--path/-p <path/to/step/dir>')
            print()
            print(bold('Example:'))
            print()
            print('  % cd mflowgen/steps')
            print('  % mkdir build && cd build')
            print('  % mflowgen mock init --path ../synopsys-dc-synthesis')
            print()
            print('Creates a mock-up graph to help develop a modular step.')
            print('The mock-up contains the "design-under-test" node and a')
            print('"push" node that connects to the inputs. You can place')
            print('inputs to your step in this node with full access to')
            print('normal build targets (e.g., make status) to make sure')
            print('your step works.')
            print()

        if help_ or not path:
            print_help()
            return

        # Make sure we are not building while nested inside the step itself
        #
        # This will lead to a possibly infinite recursive copy
        #

        try:
            assert os.path.abspath(path) not in os.path.abspath('.')
        except AssertionError:
            print()
            print(bold('Error:'), 'Nesting a mock build within the target')
            print('directory given by --path is currently not allowed.')
            print()
            sys.exit(1)

        # Make sure the given path points to a step with a configure.yml

        try:
            assert os.path.exists(path + '/configure.yml')
        except AssertionError:
            print()
            print(bold('Error:'), 'Option --path must point to a directory')
            print('that has a step configuration file "configure.yml"')
            print()
            sys.exit(1)

        # Copy the construct.py template and mock-push step to the current
        # directory

        mock_src_dir = os.path.dirname(__file__)

        construct_template = 'construct.py.template'
        mock_push_template = 'mock-push'

        construct_template_dst = './' + construct_template
        mock_push_template_dst = './' + mock_push_template

        try:
            os.remove(construct_template_dst)  # force replace
        except FileNotFoundError:
            pass
        try:
            shutil.rmtree(mock_push_template_dst)  # force replace
        except FileNotFoundError:
            pass

        try:
            shutil.copy2(src=mock_src_dir + '/' + construct_template,
                         dst=construct_template_dst)
            shutil.copytree(src=mock_src_dir + '/' + mock_push_template,
                            dst=mock_push_template_dst)
        except Exception as e:
            print(bold('Error:'), 'Failed to copy from mflowgen src')
            raise

        # Fill in the construct.py template for the given step

        with open('construct.py', 'w') as f1:
            with open(construct_template_dst) as f2:
                text = f2.read()
            f1.write(text.format(path=path))

        # Launch mflowgen run on the mock graph

        RunHandler().launch(help_=False, design='.')
Beispiel #22
0
  def launch_pull( s, help_, hash_ ):

    # Help message

    def print_help():
      print()
      print( bold( 'Usage:' ), 'mflowgen stash pull --hash <hash>'       )
      print()
      print( bold( 'Example:' ), 'mflowgen stash pull --hash 3e5ab4'     )
      print()
      print( 'Pulls a pre-built step from the stash matching the given'  )
      print( 'hash. This command copies the pre-built step from the'     )
      print( 'stash using archive mode (preserves all permissions). The' )
      print( 'new step replaces the same step in the existing graph.'    )
      print( 'For example if the existing graph marks'                   )
      print( '"synopsys-dc-synthesis" as step 4 and a pre-built'         )
      print( '"synopsys-dc-synthesis" is pulled from the stash, the'     )
      print( 'existing build directory is removed and the pre-built'     )
      print( 'version replaces it as step 4. The status of the'          )
      print( 'pre-built step is forced to be up to date until the step'  )
      print( 'is cleaned.'                                               )
      print()

    if help_ or not hash_:
      print_help()
      return

    # Sanity-check the stash

    s.verify_stash()

    # Get the step metadata

    ind  = s.get_hash_index_in_stash( hash_ )
    data = s.stash[ ind ]
    step = data[ 'step' ]

    # Get the build directory for the matching configured step
    #
    # Currently this is done by just looking at the directory names in the
    # hidden mflowgen metadata directory, which has all the build
    # directories for the current configuration.
    #

    existing_steps = os.listdir( '.mflowgen' )

    m = [ re.match( r'^(\d+)-' + step + '$', _ ) \
            for _ in existing_steps ]  # e.g., "4-synopsys-dc-synthesis"
    m = [ _ for _ in m if _ ]          # filter for successful matches
    m = [ _.group(0) for _ in m if _ ] # get build directories

    try:
      assert len( m ) > 0   # Assert for at least one match
    except AssertionError:
      print( bold( 'Error:' ), 'The currently configured graph',
              'does not contain step "{}"'.format( step ) )
      sys.exit( 1 )

    build_dir = m[0]

    # Remove the build directory if it exists

    shutil.rmtree( path = build_dir, ignore_errors = True )

    # Now copy from the stash
    #
    # - symlinks                 = False # Follow all symlinks
    # - ignore_dangling_symlinks = False # Stop with error if we cannot
    #                                    #  follow a link to something
    #                                    #  we need
    #

    remote_path = s.get_stash_path() + '/' + data[ 'dir' ]

    try:
      shutil.copytree( src      = remote_path,
                       dst      = build_dir,
                       symlinks = False,
                       ignore_dangling_symlinks = False )
    except Exception as e:
      print( bold( 'Error:' ), 'Failed to complete stash pull' )
      raise

    # Mark the new step as pre-built with a ".prebuilt" flag

    with open( build_dir + '/.prebuilt', 'w' ) as fd: # touch
      pass

    print(
      'Pulled step "{step}" from stash into "{dir_}"'.format(
      step = step,
      dir_ = build_dir,
    ) )
Beispiel #23
0
  def launch_push( s, help_, step, msg ):

    try:
      author = os.environ[ 'USER' ]
    except KeyError:
      author = 'Unknown'

    # Help message

    def print_help():
      print()
      print( bold( 'Usage:' ), 'mflowgen stash push',
                                  '--step/-s <int> --message/-m "<str>"' )
      print()
      print( bold( 'Example:' ), 'mflowgen stash push',
                                    '--step 5 -m "foo bar"'              )
      print()
      print( 'Pushes a built step to the mflowgen stash. The given step' )
      print( 'is copied to the stash using archive mode (preserves all'  )
      print( 'permissions) while following all symlinks. Then the'       )
      print( 'stashed copy is given a hash stamp and is marked as'       )
      print( 'authored by $USER (' + author + '). An optional message'   )
      print( 'can also be attached to the push.'                         )
      print()

    if help_ or not step or not msg:
      print_help()
      return

    # Sanity-check the stash

    s.verify_stash()

    # Get step to push
    #
    # Check the current directory and search for a dirname matching the
    # given step number.
    #
    # - E.g., if "step" is 5 then look for any sub-directory that starts
    #   with "5-". If there is a directory called
    #   '4-synopsys-dc-synthesis' then "step_name" will be
    #   "synopsys-dc-synthesis"
    #

    build_dirs = os.listdir( '.' )
    targets = [ _ for _ in build_dirs if _.startswith( str(step)+'-' ) ]

    try:
      push_target = targets[0]
    except IndexError:
      print( bold( 'Error:' ), 'No build directory found for step',
                                  '{}'.format( step ) )
      sys.exit( 1 )

    # Create a unique name for the stashed copy of this step

    today       = datetime.today()
    datestamp   = datetime.strftime( today, '%Y-%m%d' )
    hashstamp   = s.gen_unique_hash()
    step_name   = '-'.join( push_target.split('-')[1:] )

    dst_dirname = '-'.join( [ datestamp, step_name, hashstamp ] )

    # Now copy src to dst
    #
    # - symlinks                 = False # Follow all symlinks
    # - ignore_dangling_symlinks = False # Stop with error if we cannot
    #                                    #  follow a link to something
    #                                    #  we need
    #

    remote_path = s.get_stash_path() + '/' + dst_dirname

    try:
      shutil.copytree( src      = push_target,
                       dst      = remote_path,
                       symlinks = False,
                       ignore_dangling_symlinks = False )
    except shutil.Error:
      # According to online discussion, ignore_dangling_symlinks does not
      # apply recursively within sub-directories (a bug):
      #
      # - https://bugs.python.org/issue38523
      #
      # But according to more online discussion, copytree finishes copying
      # everything else before raising the exception.
      #
      # - https://bugs.python.org/issue6547
      #
      # So if we just pass here, we will have everything except dangling
      # symlinks, which is fine for our use case. Dangling symlinks can
      # happen in a few situations:
      #
      #  1. In inputs, if users cleaned earlier dependent steps. In this
      #  situation, we are just doing our best to copy what is available.
      #
      #  2. Some symlink to somewhere we do not have permission to view.
      #  It would be nice to raise an exception in this case, but that is
      #  hard to differentiate.
      #
      pass
    except Exception as e:
      print( bold( 'Error:' ), 'Failed to complete stash push' )
      shutil.rmtree( path = remote_path, ignore_errors = True ) # clean up
      raise

    # Update the metadata in the stash

    push_metadata = {
      'date'   : datestamp,
      'dir'    : dst_dirname,
      'hash'   : hashstamp,
      'author' : author,
      'step'   : step_name,
      'msg'    : msg,
    }

    s.stash.append( push_metadata )
    s.update_stash()

    print(
      'Stashed step {step} "{step_name}" as author "{author}"'.format(
      step      = step,
      step_name = step_name,
      author    = author,
    ) )
Beispiel #24
0
    def launch_push(s, help_, step, msg, all_):

        try:
            author = os.environ['USER']
        except KeyError:
            author = 'Unknown'

        # Help message

        def print_help():
            print()
            print(bold('Usage:'), 'mflowgen stash push',
                  '--step/-s <int> --message/-m "<str>"', '[--all]')
            print()
            print(bold('Example:'), 'mflowgen stash push',
                  '--step 5 -m "foo bar"')
            print()
            print('Pushes a built step to the mflowgen stash. The given step')
            print('is copied to the stash, preserving all permissions and')
            print('following all symlinks. By default, only the outputs of a')
            print('step are stashed, but the entire step can be stashed with')
            print('--all. The stashed copy is given a hash stamp and is')
            print('marked as authored by $USER (' + author + '). An optional')
            print('message can also be attached to each push.')
            print()

        if help_ or not step or not msg:
            print_help()
            return

        # Sanity-check the stash

        s.verify_stash()

        # Get step to push
        #
        # Check the current directory and search for a dirname matching the
        # given step number.
        #
        # - E.g., if "step" is 5 then look for any sub-directory that starts
        #   with "5-". If there is a directory called
        #   '4-synopsys-dc-synthesis' then "step_name" will be
        #   "synopsys-dc-synthesis"
        #

        build_dirs = os.listdir('.')
        targets = [_ for _ in build_dirs if _.startswith(str(step) + '-')]

        try:
            push_target = targets[0]
        except IndexError:
            print(bold('Error:'), 'No build directory found for step',
                  '{}'.format(step))
            sys.exit(1)

        # Create a unique name for the stashed copy of this step

        today = datetime.today()
        datestamp = datetime.strftime(today, '%Y-%m%d')
        hashstamp = s.gen_unique_hash()
        step_name = '-'.join(push_target.split('-')[1:])

        dst_dirname = '-'.join([datestamp, step_name, hashstamp])

        # Try to get some git information to help describe "where this step
        # came from"

        def get_shell_output(cmd):
            try:
                output = subprocess.check_output(cmd.split(),
                                                 stderr=subprocess.DEVNULL,
                                                 universal_newlines=True)
                output = output.strip()
            except Exception:
                output = ''
            return output

        git_cmd = 'git rev-parse --short HEAD'  # git commit hash
        git_hash = get_shell_output(git_cmd)

        git_cmd = 'git rev-parse --show-toplevel'  # git root dir
        git_repo = get_shell_output(git_cmd)
        git_repo = os.path.basename(git_repo)

        git_info = {'git-root-dir': git_repo, 'git-hash': git_hash}

        # Helper function to ignore copying files other than the outputs

        def f_ignore(path, files):
            # For nested subdirectories, ignore all files
            if '/' in path:
                if path.split('/')[1] == 'outputs':
                    return []  # ignore nothing in outputs
                else:
                    return files  # ignore everything for any other directory
            # At the top level, keep the outputs and a few other misc files
            keep = [
                'outputs',
                'configure.yml',
                'mflowgen-run.log',
                '.time_start',
                '.time_end',
                '.stamp',
                '.execstamp',
                '.postconditions.stamp',
            ]
            ignore = [_ for _ in files if _ not in keep]
            return ignore

        # Now copy src to dst
        #
        # - symlinks                 = False  # Follow all symlinks
        # - ignore_dangling_symlinks = False  # Stop with error if we cannot
        #                                     #  follow a link to something
        #                                     #  we need
        # - ignore                   = (func) # Ignore all but the outputs
        #                                     #  unless "--all" was given
        #

        remote_path = s.get_stash_path() + '/' + dst_dirname

        try:
            shutil.copytree(src=push_target,
                            dst=remote_path,
                            symlinks=False,
                            ignore=None if all_ else f_ignore,
                            ignore_dangling_symlinks=False)
        except shutil.Error:
            # According to online discussion, ignore_dangling_symlinks does not
            # apply recursively within sub-directories (a bug):
            #
            # - https://bugs.python.org/issue38523
            #
            # But according to more online discussion, copytree finishes copying
            # everything else before raising the exception.
            #
            # - https://bugs.python.org/issue6547
            #
            # So if we just pass here, we will have everything except dangling
            # symlinks, which is fine for our use case. Dangling symlinks can
            # happen in a few situations:
            #
            #  1. In inputs, if users cleaned earlier dependent steps. In this
            #  situation, we are just doing our best to copy what is available.
            #
            #  2. Some symlink to somewhere we do not have permission to view.
            #  It would be nice to raise an exception in this case, but that is
            #  hard to differentiate.
            #
            pass
        except Exception as e:
            print(bold('Error:'), 'Failed to complete stash push')
            shutil.rmtree(path=remote_path, ignore_errors=True)  # clean up
            raise

        # Update the metadata in the stash

        push_metadata = {
            'date': datestamp,
            'dir': dst_dirname,
            'hash': hashstamp,
            'author': author,
            'step': step_name,
            'msg': msg,
            'git-info': git_info,
        }

        s.stash.append(push_metadata)
        s.update_stash()

        print('Stashed step {step} "{step_name}" as author "{author}"'.format(
            step=step,
            step_name=step_name,
            author=author,
        ))