Skip to content

Commit

Permalink
cli: stage command migrated to docopts
Browse files Browse the repository at this point in the history
This also introduces recursive lifecycle execution to properly
handle dependencies.

Signed-off-by: Sergio Schvezov <sergio.schvezov@canonical.com>
  • Loading branch information
sergiusens committed Dec 3, 2015
1 parent 02c1d08 commit f4c5430
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 129 deletions.
58 changes: 0 additions & 58 deletions snapcraft/cmds.py

This file was deleted.

39 changes: 39 additions & 0 deletions snapcraft/commands/stage.py
@@ -0,0 +1,39 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2015 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""
snapcraft stage
Stage parts.
Usage:
stage [options] [PART ...]
Options:
-h --help show this help message and exit.
"""

from docopt import docopt

from snapcraft import lifecycle


def main(argv=None):
argv = argv if argv else []
args = docopt(__doc__, argv=argv)

lifecycle.execute('stage', args['PART'])
48 changes: 48 additions & 0 deletions snapcraft/lifecycle.py
Expand Up @@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import contextlib
import filecmp
import glob
import importlib
import logging
Expand Down Expand Up @@ -48,15 +49,30 @@ def execute(step, part_names=None):
parts = {p for p in config.all_parts if p.name in part_names}
else:
parts = config.all_parts
part_names = config.part_names

step_index = common.COMMAND_ORDER.index(step) + 1
for step in common.COMMAND_ORDER[0:step_index]:
if step == 'stage':
_check_for_collisions(config.all_parts)
for part in parts:
prereqs = config.part_prereqs(part.name)
if prereqs and not prereqs.issubset(part_names):
raise RuntimeError(
'Requested {!r} of {!r} but there are prerequisites: '

This comment has been minimized.

Copy link
@chipaca

chipaca Dec 3, 2015

unsatisfied prerequisites?

This comment has been minimized.

Copy link
@sergiusens

sergiusens Dec 4, 2015

Author Owner

thanks

'{!r}'.format(step, part.name, ' '.join(prereqs)))
elif prereqs:
# prerequisites need to build all the way to the staging
# step to be able to share the common assets that make them
# a dependency.
execute('stage', prereqs)

This comment has been minimized.

Copy link
@chipaca

chipaca Dec 3, 2015

do you check that you haven't set yourself as a prerequisite?

This comment has been minimized.

Copy link
@sergiusens

sergiusens Dec 4, 2015

Author Owner

should be rather impossible. (config is considered sane). But as always, I can check just in case of bugs

common.env = config.build_env_for_part(part)
if step == 'pull':
part.pull()
elif step == 'build':
part.build()
elif step == 'stage':
part.stage()


class PluginError(Exception):
Expand Down Expand Up @@ -420,3 +436,35 @@ def _validate_relative_paths(files):
for d in files:
if os.path.isabs(d):
raise PluginError('path "{}" must be relative'.format(d))


def _check_for_collisions(parts):
parts_files = {}
for part in parts:
# Gather our own files up
part_files, _ = part.migratable_fileset_for('stage')

# Scan previous parts for collisions
for other_part_name in parts_files:
common = part_files & parts_files[other_part_name]['files']
conflict_files = []
for f in common:
this = os.path.join(part.installdir, f)
other = os.path.join(
parts_files[other_part_name]['installdir'],
f)
if os.path.islink(this) and os.path.islink(other):
continue
if not filecmp.cmp(this, other, shallow=False):
conflict_files.append(f)

if conflict_files:
raise EnvironmentError(
'Parts {!r} and {!r} have the following file paths in '
'common which have different contents:\n{}'.format(
other_part_name, part.name,
'\n'.join(sorted(conflict_files))))

# And add our files to the list
parts_files[part.name] = {'files': part_files,
'installdir': part.installdir}
46 changes: 0 additions & 46 deletions snapcraft/tests/test_cmds.py
Expand Up @@ -16,16 +16,13 @@

import logging
import os
import tempfile

import fixtures

import snapcraft.yaml

from snapcraft import (
cmds,
common,
lifecycle,
tests
)

Expand All @@ -37,49 +34,6 @@ def setUp(self):
common.set_schemadir(os.path.join(__file__,
'..', '..', '..', 'schema'))

def test_check_for_collisions(self):
fake_logger = fixtures.FakeLogger(level=logging.ERROR)
self.useFixture(fake_logger)

tmpdirObject = tempfile.TemporaryDirectory()
self.addCleanup(tmpdirObject.cleanup)
tmpdir = tmpdirObject.name

part1 = lifecycle.load_plugin('part1', 'jdk', {'source': '.'})
part1.code.installdir = tmpdir + '/install1'
os.makedirs(part1.installdir + '/a')
open(part1.installdir + '/a/1', mode='w').close()

part2 = lifecycle.load_plugin('part2', 'jdk', {'source': '.'})
part2.code.installdir = tmpdir + '/install2'
os.makedirs(part2.installdir + '/a')
with open(part2.installdir + '/1', mode='w') as f:
f.write('1')
open(part2.installdir + '/2', mode='w').close()
with open(part2.installdir + '/a/2', mode='w') as f:
f.write('a/2')

part3 = lifecycle.load_plugin('part3', 'jdk', {'source': '.'})
part3.code.installdir = tmpdir + '/install3'
os.makedirs(part3.installdir + '/a')
os.makedirs(part3.installdir + '/b')
with open(part3.installdir + '/1', mode='w') as f:
f.write('2')
with open(part2.installdir + '/2', mode='w') as f:
f.write('1')
open(part3.installdir + '/a/2', mode='w').close()

self.assertTrue(cmds._check_for_collisions([part1, part2]))
self.assertEqual('', fake_logger.output)

self.assertFalse(cmds._check_for_collisions([part1, part2, part3]))
self.assertEqual(
'Error: parts part2 and part3 have the following file paths in '
'common which have different contents:\n'
' 1\n'
' a/2\n',
fake_logger.output)

def test_load_config_with_invalid_plugin_exits_with_error(self):
fake_logger = fixtures.FakeLogger(level=logging.ERROR)
self.useFixture(fake_logger)
Expand Down

0 comments on commit f4c5430

Please sign in to comment.