Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

many: add snapcraftctl command for scriptlets #2002

Merged
merged 18 commits into from Mar 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 55 additions & 0 deletions bin/snapcraftctl
@@ -0,0 +1,55 @@
#!/bin/sh
#
# This file exists because snapcraftctl must be run using a clean environment
# that is uninfluenced by the environment of the part using it. There are a few
# reasons for this:
#
# 1. snapcraftctl is a python3 utility, but Snapcraft supports building python2
# parts, where PYTHONPATH et. al. are set for python2.
# 2. snapcraftctl is part of snapcraft, which loads up various libraries that
# can be influenced with LD_LIBRARY_PATH, which is set for many parts.
#
# Not only this, but the only way snapcraftctl works reliably is if it's run
# by exactly the same interpreter as snapcraft itself (otherwise it won't find
# snapcraft). To that end, this script will use the interpreter defined within
# the SNAPCRAFT_INTERPRETER environment variable.

# Which python3 are we using? By default, the one from the PATH. If
# SNAPCRAFT_INTERPRETER is specified, use that one instead.
python3_command="${SNAPCRAFT_INTERPRETER:-$(which python3)}"

snapcraftctl_command="""$python3_command"" -c '
import snapcraft.cli.__main__

# Click strips off the first arg by default, so the -c will not be passed
snapcraft.cli.__main__.run_snapcraftctl(prog_name=\"snapcraftctl\")
' ""$@"


# We don't actually want a 100% clean environment. Pass on the SNAP variables,
# locale settings, and environment variables required by snapcraftctl itself.
/usr/bin/env -i -- sh -<<END
# Required for snapcraftctl to actually find snapcraft when snapped via
# sitecustomize
if [ -n "$SNAP" ]; then
export SNAP="$SNAP"
fi
if [ -n "$SNAP_NAME" ]; then
export SNAP_NAME="$SNAP_NAME"
fi
if [ -n "$SNAP_VERSION" ]; then
export SNAP_VERSION="$SNAP_VERSION"
fi
if [ -n "$SNAP_ARCH" ]; then
export SNAP_ARCH="$SNAP_ARCH"
fi

# Required so Click doesn't whine about lack of a locale
export LC_ALL="$LC_ALL"
export LANG="$LANG"

# Required for snapcraftctl to work
export SNAPCRAFTCTL_CALL_FIFO="$SNAPCRAFTCTL_CALL_FIFO"
export SNAPCRAFTCTL_FEEDBACK_FIFO="$SNAPCRAFTCTL_FEEDBACK_FIFO"
$snapcraftctl_command
END
3 changes: 2 additions & 1 deletion debian/snapcraft.install
@@ -1,3 +1,4 @@
/usr/bin/snapcraft
/usr/bin/snapcraftctl
/usr/lib/python*
/usr/share/snapcraft
/usr/share/snapcraft
28 changes: 26 additions & 2 deletions schema/snapcraft.yaml
Expand Up @@ -324,13 +324,34 @@ properties:
special case, 'plugins' is also not a valid part name."
patternProperties:
^(?!plugins$)[a-z0-9][a-z0-9+-\/]*$:
# Make sure snap/prime are mutually exclusive
allOf:
# Make sure snap/prime are mutually exclusive
- not:
type: object
required: [snap, prime]
validation-failure:
"{.instance} cannot contain both 'snap' and 'prime' keywords."
"Parts cannot contain both 'snap' and 'prime' keywords."
# Make sure prepare/override-build are mutually exclusive
- not:
type: object
required: [prepare, override-build]
validation-failure:
"Parts cannot contain both 'prepare' and 'override-build'
keywords. Use 'override-build'."
# Make sure build/override-build are mutually exclusive
- not:
type: object
required: [build, override-build]
validation-failure:
"Parts cannot contain both 'build' and 'override-build'
keywords. Use 'override-build'."
# Make sure install/override-build are mutually exclusive
- not:
type: object
required: [install, override-build]
validation-failure:
"Parts cannot contain both 'install' and 'override-build'
keywords. Use 'override-build'."
type: object
minProperties: 1
properties:
Expand Down Expand Up @@ -448,6 +469,9 @@ properties:
prepare:
type: string
default: ''
override-build:
type: string
default: 'snapcraftctl build'
parse-info:
type: array
minitems: 1
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Expand Up @@ -30,6 +30,7 @@
packages = [
'snapcraft',
'snapcraft.cli',
'snapcraft.cli.snapcraftctl',
'snapcraft.extractors',
'snapcraft.integrations',
'snapcraft.internal',
Expand Down Expand Up @@ -140,6 +141,8 @@
'snapcraft-parser = snapcraft.internal.parser:main',
],
},
# This is not in console_scripts because we need a clean environment
scripts=['bin/snapcraftctl'],
data_files=[
('share/snapcraft/schema',
['schema/' + x for x in os.listdir('schema')]),
Expand Down
4 changes: 3 additions & 1 deletion snapcraft/cli/__main__.py
Expand Up @@ -21,6 +21,7 @@
import subprocess

from ._runner import run
from .snapcraftctl._runner import run as run_snapcraftctl # noqa
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, never mind, console_scripts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, didn't want to duplicate the whole "set UTF-8" thing.

from .echo import warning

# If the locale ends up being ascii, Click will barf. Let's try to prevent that
Expand All @@ -40,4 +41,5 @@
os.environ['LANG'] = 'C.UTF-8'
break

run(prog_name='snapcraft')
if __name__ == '__main__':
run(prog_name='snapcraft')
6 changes: 5 additions & 1 deletion snapcraft/cli/_runner.py
Expand Up @@ -54,12 +54,16 @@
version=snapcraft.__version__)
@click.pass_context
@add_build_options(hidden=True)
@click.option('--debug', '-d', is_flag=True)
@click.option('--debug', '-d', is_flag=True, envvar='SNAPCRAFT_DEBUG')
def run(ctx, debug, catch_exceptions=False, **kwargs):
"""Snapcraft is a delightful packaging tool."""

if debug:
log_level = logging.DEBUG

# Setting this here so that tools run within this are also in debug
# mode (e.g. snapcraftctl)
os.environ['SNAPCRAFT_DEBUG'] = 'true'
click.echo('Starting snapcraft {} from {}.'.format(
snapcraft.__version__, os.path.dirname(__file__)))
else:
Expand Down
Empty file.
80 changes: 80 additions & 0 deletions snapcraft/cli/snapcraftctl/_runner.py
@@ -0,0 +1,80 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2017 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/>.

import functools
import json
import logging
import os
import sys

import click

from snapcraft.internal.errors import SnapcraftEnvironmentError
from snapcraft.cli._errors import exception_handler
from snapcraft.internal import log


@click.group()
@click.option('--debug', '-d', is_flag=True, envvar='SNAPCRAFT_DEBUG')
def run(debug):
"""snapcraftctl is how snapcraft.yaml can communicate with snapcraft"""

if debug:
log_level = logging.DEBUG
else:
log_level = logging.INFO

# Setup global exception handler (to be called for unhandled exceptions)
sys.excepthook = functools.partial(exception_handler, debug=debug)

# In an ideal world, this logger setup would be replaced
log.configure(log_level=log_level)


@run.command()
def build():
"""Run the 'build' step of the calling part's plugin"""
_call_function('build')


def _call_function(function_name, args=None):
if not args:
args = {}

data = {
'function': function_name,
'args': args,
}

# We could load the FIFOs in `run` and shove them in the context, but
# that's too early to error out if these variables aren't defined. Doing it
# here allows one to run e.g. `snapcraftctl build --help` without needing
# these variables defined, which is a win for usability.
try:
call_fifo = os.environ['SNAPCRAFTCTL_CALL_FIFO']
feedback_fifo = os.environ['SNAPCRAFTCTL_FEEDBACK_FIFO']
except KeyError as e:
raise SnapcraftEnvironmentError(
"{!s} environment variable must be defined. Note that this "
"utility is only designed for use within a snapcraft.yaml".format(
e)) from e

with open(call_fifo, 'w') as f:
f.write(json.dumps(data))
f.flush()

with open(feedback_fifo, 'r') as f:
f.readline()
3 changes: 3 additions & 0 deletions snapcraft/internal/deprecations.py
Expand Up @@ -33,6 +33,9 @@
"in the snap.",
'dn6': "Use of the 'snap' command with a directory has been deprecated "
"in favour of the 'pack' command.",
'dn7': "The 'prepare' keyword has been replaced by 'override-build'",
'dn8': "The 'build' keyword has been replaced by 'override-build'",
'dn9': "The 'install' keyword has been replaced by 'override-build'",
}

_DEPRECATION_URL_FMT = 'http://snapcraft.io/docs/deprecation-notices/{id}'
Expand Down
18 changes: 18 additions & 0 deletions snapcraft/internal/errors.py
Expand Up @@ -549,3 +549,21 @@ def __init__(self, *, command: Union[List, str], part_name: str,
command = ' '.join(command)
super().__init__(command=command, part_name=part_name,
exit_code=exit_code)


class ScriptletBaseError(SnapcraftError):
"""Base class for all scriptlet-related exceptions.

:cvar fmt: A format string that daughter classes override

"""


class ScriptletRunError(ScriptletBaseError):
fmt = (
'Failed to run {scriptlet_name!r}: '
'Exit code was {code}.'
)

def __init__(self, scriptlet_name: str, code: int) -> None:
super().__init__(scriptlet_name=scriptlet_name, code=code)
21 changes: 11 additions & 10 deletions snapcraft/internal/pluginhandler/__init__.py
Expand Up @@ -34,7 +34,7 @@
from ._build_attributes import BuildAttributes
from ._metadata_extraction import extract_metadata
from ._plugin_loader import load_plugin # noqa
from ._scriptlets import ScriptRunner
from ._runner import Runner
from ._patchelf import PartPatcher

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -98,6 +98,13 @@ def __init__(self, *, plugin, part_properties, project_options,
self._build_attributes = BuildAttributes(
self._part_properties['build-attributes'])

self._runner = Runner(
part_properties=self._part_properties,
builddir=self.plugin.build_basedir,
builtin_functions={
'build': self.plugin.build,
})

self._migrate_state_file()

def get_pull_state(self) -> states.PullState:
Expand Down Expand Up @@ -358,15 +365,9 @@ def ignore(directory, files):
shutil.copytree(self.plugin.sourcedir, self.plugin.build_basedir,
symlinks=True, ignore=ignore)

script_runner = ScriptRunner(builddir=self.plugin.build_basedir)

script_runner.run(scriptlet=self._part_properties.get('prepare'))
build_scriptlet = self._part_properties.get('build')
if build_scriptlet:
script_runner.run(scriptlet=build_scriptlet)
else:
self.plugin.build()
script_runner.run(scriptlet=self._part_properties.get('install'))
self._runner.prepare()
self._runner.build()
self._runner.install()

self.mark_build_done()

Expand Down