diff options
author | Sam Thursfield <sam.thursfield@codethink.co.uk> | 2014-10-08 13:21:20 +0100 |
---|---|---|
committer | Sam Thursfield <sam.thursfield@codethink.co.uk> | 2014-10-08 13:21:20 +0100 |
commit | aa68a0aa5917639dd1b7f67aa21c8d8a2755459a (patch) | |
tree | 19d6bb0d013792987c9b6989519cc9d9790abacb | |
parent | 12e2cc8f3fdc7ee65cc1e50fe8acbb652d2ca866 (diff) | |
download | morph-aa68a0aa5917639dd1b7f67aa21c8d8a2755459a.tar.gz |
import: Move the processing loop into its own class
This allows keeping the state as members instead of having to pass
12+ parameters to functions.
Also in this change: you can now specify 'extra_args' for a tool
which are passed at the start of the commandline. Some tools (e.g.
Omnibus) require more than just the source repo and name to generate
a chunk morph or lorry file.
-rw-r--r-- | import/main.py | 582 |
1 files changed, 293 insertions, 289 deletions
diff --git a/import/main.py b/import/main.py index 684f7060..af7d6979 100644 --- a/import/main.py +++ b/import/main.py @@ -29,7 +29,6 @@ import json import logging import os import pipes -import subprocess import sys import time @@ -337,144 +336,117 @@ def run_extension(filename, args, cwd='.'): return '\n'.join(output) -class BaserockImportApplication(cliapp.Application): - def add_settings(self): - self.settings.string(['lorries-dir'], - "location for Lorry files", - metavar="PATH", - default=os.path.abspath('./lorries')) - self.settings.string(['definitions-dir'], - "location for morphology files", - metavar="PATH", - default=os.path.abspath('./definitions')) - self.settings.string(['checkouts-dir'], - "location for Git checkouts", - metavar="PATH", - default=os.path.abspath('./checkouts')) +class ImportLoop(object): + '''Import a package and all of its dependencies into Baserock. - self.settings.boolean(['update-existing'], - "update all the checked-out Git trees and " - "generated definitions", - default=False) - self.settings.boolean(['use-master-if-no-tag'], - "if the correct tag for a version can't be " - "found, use 'master' instead of raising an " - "error", - default=False) + This class holds the state for the processing loop. - def _stream_has_colours(self, stream): - # http://blog.mathieu-leplatre.info/colored-output-in-console-with-python.html - if not hasattr(stream, "isatty"): - return False - if not stream.isatty(): - return False # auto color only on TTYs - try: - import curses - curses.setupterm() - return curses.tigetnum("colors") > 2 - except: - # guess false in case of error - return False + ''' - def setup(self): - self.add_subcommand('omnibus', self.import_omnibus, - arg_synopsis='REPO PROJECT_NAME SOFTWARE_NAME') - self.add_subcommand('rubygems', self.import_rubygems, - arg_synopsis='GEM_NAME') + def __init__(self, app, kind, goal_name, goal_version, extra_args=[]): + self.app = app + self.kind = kind + self.goal_name = goal_name + self.goal_version = goal_version + self.extra_args = extra_args - self.stdout_has_colours = self._stream_has_colours(sys.stdout) + self.lorry_set = LorrySet(self.app.settings['lorries-dir']) + self.morph_set = MorphologySet(self.app.settings['definitions-dir']) - def setup_logging_formatter_for_file(self): - root_logger = logging.getLogger() - root_logger.name = 'main' + self.morphloader = morphlib.morphloader.MorphologyLoader() - # You need recent cliapp for this to work, with commit "Split logging - # setup into further overrideable methods". - return logging.Formatter("%(name)s: %(levelname)s: %(message)s") + def run(self): + '''Process the goal package and all of its dependencies.''' + start_time = time.time() + start_displaytime = time.strftime('%x %X %Z', time.localtime()) - def process_args(self, args): - if len(args) == 0: - # Cliapp default is to just say "ERROR: must give subcommand" if - # no args are passed, I prefer this. - args = ['help'] + self.app.status( + '%s: Import of %s %s started', start_displaytime, self.kind, + self.goal_name) - super(BaserockImportApplication, self).process_args(args) + if not self.app.settings['update-existing']: + self.app.status( + 'Not updating existing Git checkouts or existing definitions') - def status(self, msg, *args, **kwargs): - text = msg % args - if kwargs.get('error') == True: - logging.error(text) - if self.stdout_has_colours: - sys.stdout.write(ansicolor.red(text)) - else: - sys.stdout.write(text) - else: - logging.info(text) - sys.stdout.write(text) - sys.stdout.write('\n') + chunk_dir = os.path.join(self.morph_set.path, 'strata', self.goal_name) + if not os.path.exists(chunk_dir): + os.makedirs(chunk_dir) - def import_omnibus(self, args): - '''Import a software component from an Omnibus project. + to_process = [Package(self.goal_name, self.goal_version)] + processed = networkx.DiGraph() - Omnibus is a tool for generating application bundles for various - platforms. See <https://github.com/opscode/omnibus> for more - information. + errors = {} - ''' - if len(args) != 3: - raise cliapp.AppException( - 'Please give the location of the Omnibus definitions repo, ' - 'and the name of the project and the top-level software ' - 'component.') + while len(to_process) > 0: + current_item = to_process.pop() - def running_inside_bundler(): - return 'BUNDLE_GEMFILE' in os.environ + try: + build_deps, runtime_deps = self._process_package(current_item) + except BaserockImportException as e: + self.app.status(str(e), error=True) + errors[current_item] = e + build_deps = runtime_deps = {} - def command_to_run_python_in_directory(directory, args): - # Bundler requires that we run it from the Omnibus project - # directory. That messes up any relative paths the user may have - # passed on the commandline, so we do a bit of a hack to change - # back to the original directory inside the `bundle exec` process. - subshell_command = "(cd %s; exec python %s)" % \ - (pipes.quote(directory), ' '.join(map(pipes.quote, args))) - shell_command = "sh -c %s" % pipes.quote(subshell_command) - return shell_command + processed.add_node(current_item) - def reexecute_self_with_bundler(path): - script = sys.argv[0] + self._process_dependency_list( + current_item, build_deps, to_process, processed, True) + self._process_dependency_list( + current_item, runtime_deps, to_process, processed, False) - logging.info('Reexecuting %s within Bundler, so that extensions ' - 'use the correct dependencies for Omnibus and the ' - 'Omnibus project definitions.', script) - command = command_to_run_python_in_directory(os.getcwd(), sys.argv) + if len(errors) > 0: + self.app.status( + '\nErrors encountered, not generating a stratum morphology.') + self.app.status( + 'See the README files for guidance.') + else: + self._generate_stratum_morph_if_none_exists( + processed, self.goal_name) - logging.debug('Running: `bundle exec %s` in dir %s', command, path) - os.chdir(path) - os.execvp('bundle', [script, 'exec', command]) + duration = time.time() - start_time + end_displaytime = time.strftime('%x %X %Z', time.localtime()) - # Omnibus definitions are spread across multiple repos, and there is - # no stability guarantee for the definition format. The official advice - # is to use Bundler to execute Omnibus, so let's do that. - if not running_inside_bundler(): - reexecute_self_with_bundler(args[0]) + self.app.status( + '%s: Import of %s %s ended (took %i seconds)', end_displaytime, + self.kind, self.goal_name, duration) - # This is a slightly unhappy way of passing both the repo path and - # the project name in one argument. - definitions_dir = '%s#%s' % (args[0], args[1]) + def _get_dependencies_from_morphology(self, morphology, field_name): + # We need to validate this field because it doesn't go through the + # normal MorphologyFactory validation, being an extension. + value = morphology.get(field_name, {}) + if not hasattr(value, 'iteritems'): + value_type = type(value).__name__ + raise cliapp.AppException( + "Morphology for %s has invalid '%s': should be a dict, but " + "got a %s." % (morphology['name'], field_name, value_type)) - self.import_package_and_all_dependencies( - 'omnibus', args[2], definitions_dir=definitions_dir) + return value - def import_rubygems(self, args): - '''Import one or more RubyGems.''' - if len(args) != 1: - raise cliapp.AppException( - 'Please pass the name of a RubyGem on the commandline.') + def _process_package(self, package): + name = package.name + version = package.version + + lorry = self._find_or_create_lorry_file(name) + source_repo, url = self._fetch_or_update_source(lorry) + + checked_out_version, ref = self._checkout_source_version( + source_repo, name, version) + package.set_version_in_use(checked_out_version) + + chunk_morph = self._find_or_create_chunk_morph( + name, checked_out_version, source_repo, url, ref) + + package.set_morphology(chunk_morph) - self.import_package_and_all_dependencies('rubygems', args[0]) + build_deps = self._get_dependencies_from_morphology( + chunk_morph, 'x-build-dependencies-%s' % self.kind) + runtime_deps = self._get_dependencies_from_morphology( + chunk_morph, 'x-runtime-dependencies-%s' % self.kind) - def process_dependency_list(self, current_item, deps, to_process, - processed, these_are_build_deps): + return build_deps, runtime_deps + + def _process_dependency_list(self, current_item, deps, to_process, + processed, these_are_build_deps): # All deps are added as nodes to the 'processed' graph. Runtime # dependencies only need to appear in the stratum, but build # dependencies have ordering constraints, so we add edges in @@ -501,132 +473,15 @@ class BaserockImportApplication(cliapp.Application): dep_package.set_is_build_dep(True) processed.add_edge(dep_package, current_item) - def get_dependencies_from_morphology(self, morphology, field_name): - # We need to validate this field because it doesn't go through the - # normal MorphologyFactory validation, being an extension. - value = morphology.get(field_name, {}) - if not hasattr(value, 'iteritems'): - value_type = type(value).__name__ - raise cliapp.AppException( - "Morphology for %s has invalid '%s': should be a dict, but " - "got a %s." % (morphology['name'], field_name, value_type)) - - return value - - def import_package_and_all_dependencies(self, kind, goal_name, - goal_version='master', - definitions_dir=None): - start_time = time.time() - start_displaytime = time.strftime('%x %X %Z', time.localtime()) - - self.status('%s: Import of %s %s started', start_displaytime, kind, - goal_name) - - if not self.settings['update-existing']: - self.status('Not updating existing Git checkouts or existing definitions') - - lorry_set = LorrySet(self.settings['lorries-dir']) - morph_set = MorphologySet(self.settings['definitions-dir']) - - chunk_dir = os.path.join(morph_set.path, 'strata', goal_name) - if not os.path.exists(chunk_dir): - os.makedirs(chunk_dir) - - to_process = [Package(goal_name, goal_version)] - processed = networkx.DiGraph() - - errors = {} - - while len(to_process) > 0: - current_item = to_process.pop() - name = current_item.name - version = current_item.version - - try: - lorry = self.find_or_create_lorry_file(lorry_set, kind, name, - definitions_dir=definitions_dir) - - source_repo, url = self.fetch_or_update_source(lorry) - - if definitions_dir is None: - # Package is defined in the project's actual repo. - package_definition_repo = source_repo - checked_out_version, ref = self.checkout_source_version( - source_repo, name, version) - current_item.set_version_in_use(checked_out_version) - else: - # FIXME: we do actually need to make the package source - # available too so we can detect which SHA1 to build. - # For now we lie - checked_out_version = 'lie' - ref = 'lie' - # If 'definitions_dir' was passed, we assume all packages - # are defined in the same repo. - package_definition_repo = GitDirectory(definitions_dir) - - chunk_morph = self.find_or_create_chunk_morph( - morph_set, goal_name, kind, name, checked_out_version, - package_definition_repo, url, ref, definitions_dir) - - current_item.set_morphology(chunk_morph) - - build_deps = self.get_dependencies_from_morphology( - chunk_morph, 'x-build-dependencies-%s' % kind) - runtime_deps = self.get_dependencies_from_morphology( - chunk_morph, 'x-runtime-dependencies-%s' % kind) - except BaserockImportException as e: - self.status(str(e), error=True) - errors[current_item] = e - build_deps = runtime_deps = {} - - processed.add_node(current_item) - - self.process_dependency_list( - current_item, build_deps, to_process, processed, True) - self.process_dependency_list( - current_item, runtime_deps, to_process, processed, False) - - if len(errors) > 0: - self.status( - '\nErrors encountered, not generating a stratum morphology.') - self.status( - 'See the README files for guidance.') - else: - self.generate_stratum_morph_if_none_exists(processed, goal_name) - - duration = time.time() - start_time - end_displaytime = time.strftime('%x %X %Z', time.localtime()) - - self.status('%s: Import of %s %s ended (took %i seconds)', - end_displaytime, kind, goal_name, duration) - - def generate_lorry_for_package(self, kind, name, definitions_dir): - tool = '%s.to_lorry' % kind - self.status('Calling %s to generate lorry for %s', tool, name) - if definitions_dir is None: - cwd = '.' - args = [name] - else: - cwd = definitions_dir.rsplit('#')[0] - args = [definitions_dir, name] - lorry_text = run_extension(tool, args, cwd=cwd) - try: - lorry = json.loads(lorry_text) - except ValueError as e: - raise cliapp.AppException( - 'Invalid output from %s: %s' % (tool, lorry_text)) - return lorry - - def find_or_create_lorry_file(self, lorry_set, kind, name, - definitions_dir): + def _find_or_create_lorry_file(self, name): # Note that the lorry file may already exist for 'name', but lorry # files are named for project name rather than package name. In this # case we will generate the lorry, and try to add it to the set, at # which point LorrySet will notice the existing one and merge the two. - lorry = lorry_set.find_lorry_for_package(kind, name) + lorry = self.lorry_set.find_lorry_for_package(self.kind, name) if lorry is None: - lorry = self.generate_lorry_for_package(kind, name, definitions_dir) + lorry = self._generate_lorry_for_package(name) if len(lorry) != 1: raise Exception( @@ -645,7 +500,7 @@ class BaserockImportApplication(cliapp.Application): raise cliapp.AppException( 'Invalid lorry data for %s: %s' % (name, lorry)) - lorry_set.add(lorry_filename, lorry) + self.lorry_set.add(lorry_filename, lorry) else: lorry_filename = lorry.keys()[0] logging.info( @@ -653,23 +508,34 @@ class BaserockImportApplication(cliapp.Application): return lorry - def fetch_or_update_source(self, lorry): + def _generate_lorry_for_package(self, name): + tool = '%s.to_lorry' % self.kind + self.app.status('Calling %s to generate lorry for %s', tool, name) + lorry_text = run_extension(tool, self.extra_args + [name]) + try: + lorry = json.loads(lorry_text) + except ValueError as e: + raise cliapp.AppException( + 'Invalid output from %s: %s' % (tool, lorry_text)) + return lorry + + def _fetch_or_update_source(self, lorry): assert len(lorry) == 1 lorry_entry = lorry.values()[0] url = lorry_entry['url'] reponame = os.path.basename(url.rstrip('/')) - repopath = os.path.join(self.settings['checkouts-dir'], reponame) + repopath = os.path.join(self.app.settings['checkouts-dir'], reponame) # FIXME: we should use Lorry here, so that we can import other VCSes. # But for now, this hack is fine! if os.path.exists(repopath): - if self.settings['update-existing']: - self.status('Updating repo %s', url) + if self.app.settings['update-existing']: + self.app.status('Updating repo %s', url) cliapp.runcmd(['git', 'remote', 'update', 'origin'], cwd=repopath) else: - self.status('Cloning repo %s', url) + self.app.status('Cloning repo %s', url) try: cliapp.runcmd(['git', 'clone', '--quiet', url, repopath]) except cliapp.AppException as e: @@ -687,7 +553,7 @@ class BaserockImportApplication(cliapp.Application): '%s exists but is not the root of a Git repository' % repopath) return repo, url - def checkout_source_version(self, source_repo, name, version): + def _checkout_source_version(self, source_repo, name, version): # FIXME: we need to be a bit smarter than this. Right now we assume # that 'version' is a valid Git ref. @@ -703,7 +569,7 @@ class BaserockImportApplication(cliapp.Application): ref = tag_name break else: - if self.settings['use-master-if-no-tag']: + if self.app.settings['use-master-if-no-tag']: logging.warning( "Couldn't find tag %s in repo %s. Using 'master'.", tag_name, source_repo) @@ -715,45 +581,24 @@ class BaserockImportApplication(cliapp.Application): return version, ref - def generate_chunk_morph_for_package(self, kind, source_repo, name, - version, filename, definitions_dir): - tool = '%s.to_chunk' % kind - self.status('Calling %s to generate chunk morph for %s', tool, name) - - if definitions_dir is None: - cwd = '.' - args = [source_repo.dirname, name] - else: - cwd = definitions_dir.rsplit('#')[0] - args = [definitions_dir, name] - - if version != 'master': - args.append(version) - - text = run_extension(tool, args, cwd=cwd) - - loader = morphlib.morphloader.MorphologyLoader() - return loader.load_from_string(text, filename) - - def find_or_create_chunk_morph(self, morph_set, goal_name, kind, name, - version, source_repo, repo_url, named_ref, - definitions_dir): + def _find_or_create_chunk_morph(self, name, version, source_repo, repo_url, + named_ref): morphology_filename = 'strata/%s/%s-%s.morph' % ( - goal_name, name, version) + self.goal_name, name, version) + # HACK so omnibus works! #sha1 = source_repo.resolve_ref_to_commit(named_ref) sha1 = 1 def generate_morphology(): - morphology = self.generate_chunk_morph_for_package( - kind, source_repo, name, version, morphology_filename, - definitions_dir) - morph_set.save_morphology(morphology_filename, morphology) + morphology = self._generate_chunk_morph_for_package( + source_repo, name, version, morphology_filename) + self.morph_set.save_morphology(morphology_filename, morphology) return morphology - if self.settings['update-existing']: + if self.app.settings['update-existing']: morphology = generate_morphology() else: - morphology = morph_set.get_morphology( + morphology = self.morph_set.get_morphology( repo_url, sha1, morphology_filename) if morphology is None: @@ -762,13 +607,13 @@ class BaserockImportApplication(cliapp.Application): # morph. So the first time we touch a chunk morph we need to # set this info. logging.debug("Didn't find morphology for %s|%s|%s", repo_url, - sha1, morphology_filename) - morphology = morph_set.get_morphology( + sha1, morphology_filename) + morphology = self.morph_set.get_morphology( None, None, morphology_filename) if morphology is None: logging.debug("Didn't find morphology for None|None|%s", - morphology_filename) + morphology_filename) morphology = generate_morphology() morphology.repo_url = repo_url @@ -777,7 +622,20 @@ class BaserockImportApplication(cliapp.Application): return morphology - def sort_chunks_by_build_order(self, graph): + def _generate_chunk_morph_for_package(self, source_repo, name, version, + filename): + tool = '%s.to_chunk' % self.kind + self.app.status( + 'Calling %s to generate chunk morph for %s', tool, name) + + args = self.extra_args + [source_repo.dirname, name] + if version != 'master': + args.append(version) + text = run_extension(tool, args) + + return self.morphloader.load_from_string(text, filename) + + def _sort_chunks_by_build_order(self, graph): order = reversed(sorted(graph.nodes())) try: return networkx.topological_sort(graph, nbunch=order) @@ -794,20 +652,22 @@ class BaserockImportApplication(cliapp.Application): 'One or more cycles detected in build graph: %s' % (', '.join(all_loops_str))) - def generate_stratum_morph_if_none_exists(self, graph, goal_name): + def _generate_stratum_morph_if_none_exists(self, graph, goal_name): filename = os.path.join( - self.settings['definitions-dir'], 'strata', '%s.morph' % goal_name) + self.app.settings['definitions-dir'], 'strata', '%s.morph' % + goal_name) if os.path.exists(filename) and not self.settings['update-existing']: - self.status(msg='Found stratum morph for %s at %s, not overwriting' - % (goal_name, filename)) + self.app.status( + msg='Found stratum morph for %s at %s, not overwriting' % + (goal_name, filename)) return - self.status(msg='Generating stratum morph for %s' % goal_name) + self.app.status(msg='Generating stratum morph for %s' % goal_name) chunk_entries = [] - for package in self.sort_chunks_by_build_order(graph): + for package in self._sort_chunks_by_build_order(graph): m = package.morphology if m is None: raise cliapp.AppException('No morphology for %s' % package) @@ -842,12 +702,156 @@ class BaserockImportApplication(cliapp.Application): 'chunks': chunk_entries, } - loader = morphlib.morphloader.MorphologyLoader() - morphology = loader.load_from_string(json.dumps(stratum), - filename=filename) + morphology = self.morphloader.load_from_string( + json.dumps(stratum), filename=filename) + self.morphloader.unset_defaults(morphology) + self.morphloader.save_to_file(filename, morphology) + + +class BaserockImportApplication(cliapp.Application): + def add_settings(self): + self.settings.string(['lorries-dir'], + "location for Lorry files", + metavar="PATH", + default=os.path.abspath('./lorries')) + self.settings.string(['definitions-dir'], + "location for morphology files", + metavar="PATH", + default=os.path.abspath('./definitions')) + self.settings.string(['checkouts-dir'], + "location for Git checkouts", + metavar="PATH", + default=os.path.abspath('./checkouts')) + + self.settings.boolean(['update-existing'], + "update all the checked-out Git trees and " + "generated definitions", + default=False) + self.settings.boolean(['use-master-if-no-tag'], + "if the correct tag for a version can't be " + "found, use 'master' instead of raising an " + "error", + default=False) + + def _stream_has_colours(self, stream): + # http://blog.mathieu-leplatre.info/colored-output-in-console-with-python.html + if not hasattr(stream, "isatty"): + return False + if not stream.isatty(): + return False # auto color only on TTYs + try: + import curses + curses.setupterm() + return curses.tigetnum("colors") > 2 + except: + # guess false in case of error + return False + + def setup(self): + self.add_subcommand('omnibus', self.import_omnibus, + arg_synopsis='REPO PROJECT_NAME SOFTWARE_NAME') + self.add_subcommand('rubygems', self.import_rubygems, + arg_synopsis='GEM_NAME') + + self.stdout_has_colours = self._stream_has_colours(sys.stdout) + + def setup_logging_formatter_for_file(self): + root_logger = logging.getLogger() + root_logger.name = 'main' + + # You need recent cliapp for this to work, with commit "Split logging + # setup into further overrideable methods". + return logging.Formatter("%(name)s: %(levelname)s: %(message)s") + + def process_args(self, args): + if len(args) == 0: + # Cliapp default is to just say "ERROR: must give subcommand" if + # no args are passed, I prefer this. + args = ['help'] + + super(BaserockImportApplication, self).process_args(args) + + def status(self, msg, *args, **kwargs): + text = msg % args + if kwargs.get('error') == True: + logging.error(text) + if self.stdout_has_colours: + sys.stdout.write(ansicolor.red(text)) + else: + sys.stdout.write(text) + else: + logging.info(text) + sys.stdout.write(text) + sys.stdout.write('\n') + + def import_omnibus(self, args): + '''Import a software component from an Omnibus project. + + Omnibus is a tool for generating application bundles for various + platforms. See <https://github.com/opscode/omnibus> for more + information. + + ''' + if len(args) != 3: + raise cliapp.AppException( + 'Please give the location of the Omnibus definitions repo, ' + 'and the name of the project and the top-level software ' + 'component.') + + def running_inside_bundler(): + return 'BUNDLE_GEMFILE' in os.environ + + def command_to_run_python_in_directory(directory, args): + # Bundler requires that we run it from the Omnibus project + # directory. That messes up any relative paths the user may have + # passed on the commandline, so we do a bit of a hack to change + # back to the original directory inside the `bundle exec` process. + subshell_command = "(cd %s; exec python %s)" % \ + (pipes.quote(directory), ' '.join(map(pipes.quote, args))) + shell_command = "sh -c %s" % pipes.quote(subshell_command) + return shell_command + + def reexecute_self_with_bundler(path): + script = sys.argv[0] + + logging.info('Reexecuting %s within Bundler, so that extensions ' + 'use the correct dependencies for Omnibus and the ' + 'Omnibus project definitions.', script) + command = command_to_run_python_in_directory(os.getcwd(), sys.argv) + + logging.debug('Running: `bundle exec %s` in dir %s', command, path) + os.chdir(path) + os.execvp('bundle', [script, 'exec', command]) + + # Omnibus definitions are spread across multiple repos, and there is + # no stability guarantee for the definition format. The official advice + # is to use Bundler to execute Omnibus, so let's do that. + if not running_inside_bundler(): + reexecute_self_with_bundler(args[0]) + + definitions_dir = args[0] + project_name = args[1] + + ImportLoop( + app=self, + kind='omnibus', + goal_name=args[2], + goal_version='master', + extra_args=[definitions_dir, project_name] + ).run() + + def import_rubygems(self, args): + '''Import one or more RubyGems.''' + if len(args) != 1: + raise cliapp.AppException( + 'Please pass the name of a RubyGem on the commandline.') - loader.unset_defaults(morphology) - loader.save_to_file(filename, morphology) + ImportLoop( + app=self, + kind='rubygems', + goal_name=args[0], + goal_version='master' + ).run() app = BaserockImportApplication(progname='import') |