Merge ~morphis/snappy-hwe-snaps/+git/jenkins-jobs:f/kernel-cross-build-support into ~snappy-hwe-team/snappy-hwe-snaps/+git/jenkins-jobs:master

Proposed by Simon Fels
Status: Merged
Approved by: Alfonso Sanchez-Beato
Approved revision: c8dd33592bcd3af6477148d134a43800938e2b02
Merged at revision: 5d818f78ab6785efa28bb6a8776dc4f7ca5569fc
Proposed branch: ~morphis/snappy-hwe-snaps/+git/jenkins-jobs:f/kernel-cross-build-support
Merge into: ~snappy-hwe-team/snappy-hwe-snaps/+git/jenkins-jobs:master
Diff against target: 609 lines (+503/-11)
3 files modified
jobs/snap/snap-build-worker.sh (+14/-1)
tools/shyaml (+454/-0)
tools/snapbuild.sh (+35/-10)
Reviewer Review Type Date Requested Status
Alfonso Sanchez-Beato Approve
System Enablement Bot continuous-integration Approve
Review via email: mp+325979@code.launchpad.net

Description of the change

Implement cross-build support for kernel type snaps

This imports the shyaml script into the tree as we don't have another way to install shyaml in our jenkins environment (even the proxy disallows python pip access).

To post a comment you must log in.
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Alfonso Sanchez-Beato (alfonsosanchezbeato) wrote :

A couple of inline comments. Also:

1. Please add to the header of "tools/shyaml" the external repo/commit from which it was taken so we cant track the origin an update easily in the future.

2. It would be a good opportunity to clean scripts of shellcheck warnings, see
https://pastebin.canonical.com/191399/
and
https://pastebin.canonical.com/191400/

review: Needs Fixing
Revision history for this message
Simon Fels (morphis) wrote :

Let's move the shellcheck cleanup into another MP.

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Alfonso Sanchez-Beato (alfonsosanchezbeato) wrote :

Thanks for the changes. Re: shellcheck, alright, but tbh 90% of the warnings are just adding quotes around, so no big deal. We should develop the habit of running shellcheck whenever we modify a shell script, it is an incredibly helpful tool.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/jobs/snap/snap-build-worker.sh b/jobs/snap/snap-build-worker.sh
2index e393c74..0c62b2c 100644
3--- a/jobs/snap/snap-build-worker.sh
4+++ b/jobs/snap/snap-build-worker.sh
5@@ -82,12 +82,25 @@ sed -i "s/~$LAUNCHPAD_TEAM\/$LAUNCHPAD_PROJECT\/+git\/$REPO_NAME/~$LAUNCHPAD_TEA
6 # $WORKSPACE/.build_env file.
7
8 if [ "$BUILD_ON_LAUNCHPAD" = False ]; then
9+ SNAPBUILD_EXTRA_ARGS=
10+ SNAP_TYPE=$($BUILD_SCRIPTS/tools/shyaml get-value type < $SNAPCRAFT_YAML_PATH)
11+ case "$SNAP_TYPE" in
12+ kernel)
13+ if [ "$ARCH" != amd64 ]; then
14+ # If we're building a kernel snap we have to cross-build it
15+ # instead of building it in a native environment.
16+ SNAPBUILD_EXTRA_ARGS="--cross-build"
17+ fi
18+ ;;
19+ esac
20+
21 sudo $BUILD_SCRIPTS/tools/snapbuild.sh \
22 --source-dir=$WORKSPACE/src \
23 --results-dir=$WORKSPACE/results \
24 --arch=$ARCHITECTURE \
25 --series=$SERIES \
26- --proxy=squid.internal:3128
27+ --proxy=squid.internal:3128 \
28+ $SNAPBUILD_EXTRA_ARGS
29 else
30 git remote add jenkins-ci git+ssh://$BOT_USERNAME@git.launchpad.net/~$LAUNCHPAD_TEAM/$LAUNCHPAD_PROJECT/+git/$CI_REPO
31 git push jenkins-ci --all
32diff --git a/tools/shyaml b/tools/shyaml
33new file mode 100755
34index 0000000..e4618ec
35--- /dev/null
36+++ b/tools/shyaml
37@@ -0,0 +1,454 @@
38+#!/usr/bin/env python
39+
40+# Taken from upstream git repository https://github.com/0k/shyaml
41+# at revision d77e30599a0971c51896ef97d21883550e7e9979
42+
43+## Note: to launch test, you can use:
44+## python -m doctest -d shyaml.py
45+## or
46+## nosetests
47+
48+from __future__ import print_function
49+
50+import sys
51+import yaml
52+import os.path
53+import re
54+
55+PY3 = sys.version_info[0] >= 3
56+
57+EXNAME = os.path.basename(sys.argv[0])
58+
59+USAGE = """\
60+Usage:
61+
62+ %(exname)s (-h|--help)
63+ %(exname)s [-y|--yaml] ACTION KEY [DEFAULT]
64+""" % {"exname": EXNAME}
65+
66+HELP = """
67+Parses and output chosen subpart or values from YAML input.
68+It reads YAML in stdin and will output on stdout it's return value.
69+
70+%(usage)s
71+
72+Options:
73+
74+ -y, --yaml
75+ Output only YAML safe value, more precisely, even
76+ literal values will be YAML quoted. This behavior
77+ is required if you want to output YAML subparts and
78+ further process it. If you know you have are dealing
79+ with safe literal value, then you don't need this.
80+ (Default: no safe YAML output)
81+
82+ ACTION Depending on the type of data you've targetted
83+ thanks to the KEY, ACTION can be:
84+
85+ These ACTIONs applies to any YAML type:
86+
87+ get-type ## returns a short string
88+ get-value ## returns YAML
89+
90+ This ACTION applies to 'sequence' and 'struct' YAML type:
91+
92+ get-values{,-0} ## return list of YAML
93+
94+ These ACTION applies to 'struct' YAML type:
95+
96+ keys{,-0} ## return list of YAML
97+ values{,-0} ## return list of YAML
98+ key-values,{,-0} ## return list of YAML
99+
100+ Note that any value returned is returned on stdout, and
101+ when returning ``list of YAML``, it'll be separated by
102+ ``\\n`` or ``NUL`` char depending of you've used the
103+ ``-0`` suffixed ACTION.
104+
105+ KEY Identifier to browse and target subvalues into YAML
106+ structure. Use ``.`` to parse a subvalue. If you need
107+ to use a literal ``.`` or ``\``, use ``\`` to quote it.
108+
109+ Use struct keyword to browse ``struct`` YAML data and use
110+ integers to browse ``sequence`` YAML data.
111+
112+ DEFAULT if not provided and given KEY do not match any value in
113+ the provided YAML, then DEFAULT will be returned. If no
114+ default is provided and the KEY do not match any value
115+ in the provided YAML, %(exname)s will fail with an error
116+ message.
117+
118+Examples:
119+
120+ ## get last grocery
121+ cat recipe.yaml | %(exname)s get-value groceries.-1
122+
123+ ## get all words of my french dictionary
124+ cat dictionaries.yaml | %(exname)s keys-0 french.dictionary
125+
126+ ## get YAML config part of 'myhost'
127+ cat hosts_config.yaml | %(exname)s get-value cfgs.myhost
128+
129+""" % {"exname": EXNAME, "usage": USAGE}
130+
131+##
132+## Keep previous order in YAML
133+##
134+
135+try:
136+ # included in standard lib from Python 2.7
137+ from collections import OrderedDict
138+except ImportError:
139+ # try importing the backported drop-in replacement
140+ # it's available on PyPI
141+ from ordereddict import OrderedDict
142+
143+
144+## Ensure that there are no collision with legacy OrderedDict
145+## that could be used for omap for instance.
146+class MyOrderedDict(OrderedDict):
147+ pass
148+
149+yaml.add_representer(
150+ MyOrderedDict,
151+ lambda cls, data: cls.represent_dict(data.items()))
152+
153+
154+def construct_omap(cls, node):
155+ ## Force unfolding reference and merges
156+ ## otherwise it would fail on 'merge'
157+ cls.flatten_mapping(node)
158+ return MyOrderedDict(cls.construct_pairs(node))
159+
160+
161+yaml.add_constructor(
162+ yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
163+ construct_omap)
164+
165+
166+##
167+## Key specifier
168+##
169+
170+def tokenize(s):
171+ r"""Returns an iterable through all subparts of string splitted by '.'
172+
173+ So:
174+
175+ >>> list(tokenize('foo.bar.wiz'))
176+ ['foo', 'bar', 'wiz']
177+
178+ Contrary to traditional ``.split()`` method, this function has to
179+ deal with any type of data in the string. So it actually
180+ interprets the string. Characters with meaning are '.' and '\'.
181+ Both of these can be included in a token by quoting them with '\'.
182+
183+ So dot of slashes can be contained in token:
184+
185+ >>> print('\n'.join(tokenize(r'foo.dot<\.>.slash<\\>')))
186+ foo
187+ dot<.>
188+ slash<\>
189+
190+ Notice that empty keys are also supported:
191+
192+ >>> list(tokenize(r'foo..bar'))
193+ ['foo', '', 'bar']
194+
195+ Given an empty string:
196+
197+ >>> list(tokenize(r''))
198+ ['']
199+
200+ And a None value:
201+
202+ >>> list(tokenize(None))
203+ []
204+
205+ """
206+ if s is None:
207+ raise StopIteration
208+ tokens = (re.sub(r'\\(\\|\.)', r'\1', m.group(0))
209+ for m in re.finditer(r'((\\.|[^.\\])*)', s))
210+ ## an empty string superfluous token is added after all non-empty token
211+ for token in tokens:
212+ if len(token) != 0:
213+ next(tokens)
214+ yield token
215+
216+
217+def mget(dct, key):
218+ r"""Allow to get values deep in recursive dict with doted keys
219+
220+ Accessing leaf values is quite straightforward:
221+
222+ >>> dct = {'a': {'x': 1, 'b': {'c': 2}}}
223+ >>> mget(dct, 'a.x')
224+ 1
225+ >>> mget(dct, 'a.b.c')
226+ 2
227+
228+ But you can also get subdict if your key is not targeting a
229+ leaf value:
230+
231+ >>> mget(dct, 'a.b')
232+ {'c': 2}
233+
234+ As a special feature, list access is also supported by providing a
235+ (possibily signed) integer, it'll be interpreted as usual python
236+ sequence access using bracket notation:
237+
238+ >>> mget({'a': {'x': [1, 5], 'b': {'c': 2}}}, 'a.x.-1')
239+ 5
240+ >>> mget({'a': {'x': 1, 'b': [{'c': 2}]}}, 'a.b.0.c')
241+ 2
242+
243+ Keys that contains '.' can be accessed by escaping them:
244+
245+ >>> dct = {'a': {'x': 1}, 'a.x': 3, 'a.y': 4}
246+ >>> mget(dct, 'a.x')
247+ 1
248+ >>> mget(dct, r'a\.x')
249+ 3
250+ >>> mget(dct, r'a.y') ## doctest: +IGNORE_EXCEPTION_DETAIL
251+ Traceback (most recent call last):
252+ ...
253+ MissingKeyError: missing key 'y' in dict.
254+ >>> mget(dct, r'a\.y')
255+ 4
256+
257+ As a consequence, if your key contains a '\', you should also escape it:
258+
259+ >>> dct = {r'a\x': 3, r'a\.x': 4, 'a.x': 5, 'a\\': {'x': 6}}
260+ >>> mget(dct, r'a\\x')
261+ 3
262+ >>> mget(dct, r'a\\\.x')
263+ 4
264+ >>> mget(dct, r'a\\.x')
265+ 6
266+ >>> mget({'a\\': {'b': 1}}, r'a\\.b')
267+ 1
268+ >>> mget({r'a.b\.c': 1}, r'a\.b\\\.c')
269+ 1
270+
271+ And even empty strings key are supported:
272+
273+ >>> dct = {r'a': {'': {'y': 3}, 'y': 4}, 'b': {'': {'': 1}}, '': 2}
274+ >>> mget(dct, r'a..y')
275+ 3
276+ >>> mget(dct, r'a.y')
277+ 4
278+ >>> mget(dct, r'')
279+ 2
280+ >>> mget(dct, r'b..')
281+ 1
282+
283+ It will complain if you are trying to get into a leaf:
284+
285+ >>> mget({'a': 1}, 'a.y') ## doctest: +IGNORE_EXCEPTION_DETAIL
286+ Traceback (most recent call last):
287+ ...
288+ NonDictLikeTypeError: can't query subvalue 'y' of a leaf...
289+
290+ if the key is None, the whole dct should be sent back:
291+
292+ >>> mget({'a': 1}, None)
293+ {'a': 1}
294+
295+ """
296+ return aget(dct, tokenize(key))
297+
298+
299+class MissingKeyError(KeyError):
300+ """Raised when querying a dict-like structure on non-existing keys"""
301+
302+ def __str__(self):
303+ return self.message
304+
305+
306+class NonDictLikeTypeError(TypeError):
307+ """Raised when attempting to traverse non-dict like structure"""
308+
309+
310+class IndexNotIntegerError(ValueError):
311+ """Raised when attempting to traverse sequence without using an integer"""
312+
313+
314+class IndexOutOfRange(IndexError):
315+ """Raised when attempting to traverse sequence without using an integer"""
316+
317+
318+def aget(dct, key):
319+ r"""Allow to get values deep in a dict with iterable keys
320+
321+ Accessing leaf values is quite straightforward:
322+
323+ >>> dct = {'a': {'x': 1, 'b': {'c': 2}}}
324+ >>> aget(dct, ('a', 'x'))
325+ 1
326+ >>> aget(dct, ('a', 'b', 'c'))
327+ 2
328+
329+ If key is empty, it returns unchanged the ``dct`` value.
330+
331+ >>> aget({'x': 1}, ())
332+ {'x': 1}
333+
334+ """
335+ key = iter(key)
336+ try:
337+ head = next(key)
338+ except StopIteration:
339+ return dct
340+
341+ if isinstance(dct, list):
342+ try:
343+ idx = int(head)
344+ except ValueError:
345+ raise IndexNotIntegerError(
346+ "non-integer index %r provided on a list."
347+ % head)
348+ try:
349+ value = dct[idx]
350+ except IndexError:
351+ raise IndexOutOfRange(
352+ "index %d is out of range (%d elements in list)."
353+ % (idx, len(dct)))
354+ else:
355+ try:
356+ value = dct[head]
357+ except KeyError:
358+ ## Replace with a more informative KeyError
359+ raise MissingKeyError(
360+ "missing key %r in dict."
361+ % (head, ))
362+ except:
363+ raise NonDictLikeTypeError(
364+ "can't query subvalue %r of a leaf%s."
365+ % (head,
366+ (" (leaf value is %r)" % dct)
367+ if len(repr(dct)) < 15 else ""))
368+ return aget(value, key)
369+
370+
371+def stderr(msg):
372+ """Convenience function to write short message to stderr."""
373+ sys.stderr.write(msg)
374+
375+
376+def stdout(value):
377+ """Convenience function to write short message to stdout."""
378+ sys.stdout.write(value)
379+
380+
381+def die(msg, errlvl=1, prefix="Error: "):
382+ """Convenience function to write short message to stderr and quit."""
383+ stderr("%s%s\n" % (prefix, msg))
384+ sys.exit(errlvl)
385+
386+SIMPLE_TYPES = (str if PY3 else basestring, int, float, type(None))
387+COMPLEX_TYPES = (list, dict)
388+
389+
390+def magic_dump(value):
391+ """Returns a representation of values directly usable by bash.
392+
393+ Literal types are printed as-is (avoiding quotes around string for
394+ instance). But complex type are written in a YAML useable format.
395+
396+ """
397+ return value if isinstance(value, SIMPLE_TYPES) \
398+ else yaml.dump(value, default_flow_style=False)
399+
400+def yaml_dump(value):
401+ """Returns a representation of values directly usable by bash.
402+
403+ Literal types are quoted and safe to use as YAML.
404+
405+ """
406+ return yaml.dump(value, default_flow_style=False)
407+
408+
409+def type_name(value):
410+ """Returns pseudo-YAML type name of given value."""
411+ return "struct" if isinstance(value, dict) else \
412+ "sequence" if isinstance(value, (tuple, list)) else \
413+ type(value).__name__
414+
415+
416+def main(args): ## pylint: disable=too-many-branches
417+ """Entrypoint of the whole application"""
418+
419+ if len(args) == 0:
420+ stderr("Error: Bad number of arguments.\n")
421+ die(USAGE, errlvl=1, prefix="")
422+
423+ if len(args) == 1 and args[0] in ("-h", "--help"):
424+ stdout(HELP)
425+ exit(0)
426+
427+ dump = magic_dump
428+ for arg in ["-y", "--yaml"]:
429+ if arg in args:
430+ args.remove(arg)
431+ dump = yaml_dump
432+
433+ action = args[0]
434+ key_value = None if len(args) == 1 else args[1]
435+ default = args[2] if len(args) > 2 else None
436+ contents = yaml.load(sys.stdin)
437+
438+ try:
439+ try:
440+ value = mget(contents, key_value)
441+ except (IndexOutOfRange, MissingKeyError):
442+ if default is None:
443+ raise
444+ value = default
445+ except (IndexOutOfRange, MissingKeyError,
446+ NonDictLikeTypeError, IndexNotIntegerError) as exc:
447+ msg = str(exc.message)
448+ die("invalid path %r, %s"
449+ % (key_value,
450+ msg.replace('list', 'sequence').replace('dict', 'struct')))
451+
452+ tvalue = type_name(value)
453+ termination = "\0" if action.endswith("-0") else "\n"
454+
455+ if action == "get-value":
456+ print(dump(value), end='')
457+ elif action in ("get-values", "get-values-0"):
458+ if isinstance(value, dict):
459+ for k, v in value.iteritems():
460+ stdout("%s%s%s%s" % (dump(k), termination,
461+ dump(v), termination))
462+ elif isinstance(value, list):
463+ for l in value:
464+ stdout("%s%s" % (dump(l), termination))
465+ else:
466+ die("%s does not support %r type. "
467+ "Please provide or select a sequence or struct."
468+ % (action, tvalue))
469+ elif action == "get-type":
470+ print(tvalue)
471+ elif action in ("keys", "keys-0",
472+ "values", "values-0",
473+ "key-values", "key-values-0"):
474+ if isinstance(value, dict):
475+ method = value.keys if action.startswith("keys") else \
476+ value.items if action.startswith("key-values") else \
477+ value.values
478+ output = (lambda x: termination.join(str(dump(e)) for e in x)) \
479+ if action.startswith("key-values") else \
480+ dump
481+ for k in method():
482+ stdout("%s%s" % (output(k), termination))
483+ else:
484+ die("%s does not support %r type. "
485+ "Please provide or select a struct." % (action, tvalue))
486+ else:
487+ die("Invalid argument.\n%s" % USAGE)
488+
489+
490+if __name__ == "__main__":
491+ sys.exit(main(sys.argv[1:]))
492diff --git a/tools/snapbuild.sh b/tools/snapbuild.sh
493index 20ca763..c98fcb4 100755
494--- a/tools/snapbuild.sh
495+++ b/tools/snapbuild.sh
496@@ -24,10 +24,16 @@ fi
497 SERIES=xenial
498 SOURCE_DIR=
499 RESULTS_DIR=
500+# Whenever you change the chroot in a way which needs a regeneration
501+# on the build server bump the version here. This will tell the
502+# job which updates the chroots to generate a new one.
503 CHROOT_VERSION=1
504-ARCH=amd64
505+BUILD_ARCH=amd64
506+TARGET_ARCH=amd64
507 UPDATE_CHROOT=false
508 PROXY=
509+CROSS_BUILD=false
510+SNAPCRAFT_EXTRA_ARGS=
511
512 while [ -n "$1" ]; do
513 case "$1" in
514@@ -44,7 +50,8 @@ while [ -n "$1" ]; do
515 shift
516 ;;
517 --arch=*)
518- ARCH=${1#*=}
519+ TARGET_ARCH=${1#*=}
520+ BUILD_ARCH=$TARGET_ARCH
521 shift
522 ;;
523 --update-chroot)
524@@ -55,6 +62,10 @@ while [ -n "$1" ]; do
525 PROXY=${1#*=}
526 shift
527 ;;
528+ --cross-build)
529+ CROSS_BUILD=true
530+ shift
531+ ;;
532 *)
533 echo "ERROR: Unknown options $1"
534 exit 1
535@@ -66,8 +77,14 @@ if [ -z "$SERIES" ]; then
536 exit 1
537 fi
538
539+# If we're cross-building we have to switch the architecture we're using
540+# for our build environment to match our host arch.
541+if [ "$CROSS_BUILD" = true ]; then
542+ HOST_ARCH=$(dpkg --print-architecture)
543+fi
544+
545 CHROOT_STORE_PATH=/build/chroots
546-CHROOT_TARBALL=$SERIES-$ARCH-$CHROOT_VERSION-rootfs.tar
547+CHROOT_TARBALL=$SERIES-$BUILD_ARCH-$CHROOT_VERSION-rootfs.tar
548
549 if [ "$UPDATE_CHROOT" = true ]; then
550 if [ ! -e $CHROOT_STORE_PATH/$CHROOT_TARBALL ] ; then
551@@ -77,7 +94,7 @@ if [ "$UPDATE_CHROOT" = true ]; then
552
553 DEBOOTSTRAP=debootstrap
554 DEB_REPO_URL=
555- case "$ARCH" in
556+ case "$BUILD_ARCH" in
557 amd64)
558 DEB_REPO_URL="http://archive.ubuntu.com/ubuntu/"
559 ;;
560@@ -86,7 +103,7 @@ if [ "$UPDATE_CHROOT" = true ]; then
561 DEB_REPO_URL="http://ports.ubuntu.com/ubuntu-ports"
562 ;;
563 *)
564- echo "ERROR: Unsupported architecture $ARCH"
565+ echo "ERROR: Unsupported architecture $BUILD_ARCH"
566 exit 1
567 ;;
568 esac
569@@ -97,7 +114,7 @@ if [ "$UPDATE_CHROOT" = true ]; then
570
571 trap cleanup INT EXIT
572
573- $DEBOOTSTRAP --components=main,universe --arch $ARCH $SERIES $WORKDIR/rootfs
574+ $DEBOOTSTRAP --components=main,universe --arch $BUILD_ARCH $SERIES $WORKDIR/rootfs
575 cat << EOF > $WORKDIR/rootfs/etc/apt/sources.list.d/updates.list
576 deb $DEB_REPO_URL xenial universe
577 deb $DEB_REPO_URL xenial-updates main universe
578@@ -147,19 +164,27 @@ tar xf $CHROOT_STORE_PATH/$CHROOT_TARBALL -C $BUILDDIR
579
580 cp -ra $SOURCE_DIR $BUILDDIR/src
581
582+if [ "$CROSS_BUILD" = true ]; then
583+ SNAPCRAFT_EXTRA_ARGS="$SNAPCRAFT_EXTRA_ARGS --target-arch=$TARGET_ARCH"
584+fi
585+
586 cat << EOF > $BUILDDIR/do-build.sh
587 #!/bin/sh
588 set -ex
589 apt update
590 apt upgrade -y
591 cd /src
592+
593+# snapcraft is pretty unhappy when LC_ALL and LANG aren't set
594+export LC_ALL=C.UTF-8
595+export LANG=C.UTF-8
596+
597+# To access certain things like the snap store we need a proxy in place
598+# in some environments
599 export http_proxy=$PROXY
600 export https_proxy=$PROXY
601-SNAPCRAFT_EXTRA_ARGS=
602+
603 snapcraft clean
604-if [ -e /src/.ci-setup.sh ]; then
605- . /src/.ci-setup.sh
606-fi
607 snapcraft $SNAPCRAFT_EXTRA_ARGS
608 EOF
609 chmod +x $BUILDDIR/do-build.sh

Subscribers

People subscribed via source and target branches

to all changes: