Merge ~laney/germinate:master into germinate:master

Proposed by Iain Lane
Status: Merged
Merged at revision: 7102d51745c8f43d32144be99266a399026eb818
Proposed branch: ~laney/germinate:master
Merge into: germinate:master
Diff against target: 438 lines (+216/-4)
8 files modified
germinate/germinator.py (+102/-3)
germinate/log.py (+1/-0)
germinate/scripts/germinate_main.py (+2/-0)
germinate/tests/helpers.py (+7/-0)
germinate/tests/test_germinator.py (+34/-0)
germinate/tests/test_integration.py (+39/-1)
germinate/tests/test_seeds.py (+10/-0)
man/germinate.1 (+21/-0)
Reviewer Review Type Date Requested Status
Colin Watson Approve
Review via email: mp+334039@code.launchpad.net

Description of the change

As discussed on a few occasions, here's an initial merge proposal to support seeding of snaps in germinate.

This is basically a passthrough of "snap:" entries to .snap output files. I know we had said that we'd try to avoid using a colon, but I didn't find anything that was suitable and didn't look contrived. It requires some branches in the parser which I don't think are too bad. If it is, though, we can try harder to find another way of specifying seeded snaps.

When implementing this I've tried to keep snaps separated from debs as much as possible. They don't require any dependency expansion, we don't perform availability checks on them*, and if they appear intermingled with debs in the pre-existing output then consumers are likely to be confused by their presence.

Feedback welcome. I'll be proposing a companion livecd-rootfs MP soon.

* as discussed at the Ubuntu Rally in NYC

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

I'm still not wild about the "snap:" prefix, but I guess if nothing else works then we'll have to live with it. Could you please ensure that the new syntax is documented in man/germinate.1, though? That's the official reference for seed syntax.

review: Approve
Revision history for this message
Colin Watson (cjwatson) wrote :

Also, have you thought about how we're going to annotate classic snaps? I gather that we may want to install both classic and non-classic snaps on images ...

Revision history for this message
Iain Lane (laney) :
Revision history for this message
Iain Lane (laney) wrote :

Re-pushed, thanks for the review. This time I have added a section to the manpage - please review that.

> Also, have you thought about how we're going to annotate classic snaps? I
> gather that we may want to install both classic and non-classic snaps on
> images ...

Um, no, good point. Do those have to have their classicness specified in seed.yaml somehow? (I've asked #snappy for clarification as there's no documentation I can find. Currently waiting for an answer.)

Do you have a preference on syntax? Maybe "snap:foo/classic"? AFAICS "/" isn't used apart from in _filter_packages to specify a regex & that wouldn't apply here. For outputting I can imagine either passing that straight through, transforming it to "foo (classic)", or adding a new column in increasing order of difficulty on the livecd-rootfs side. Not that difficulty there matters too much.

Revision history for this message
Iain Lane (laney) wrote :

> Um, no, good point. Do those have to have their classicness specified in
> seed.yaml somehow? (I've asked #snappy for clarification as there's no
> documentation I can find. Currently waiting for an answer.)
>
> Do you have a preference on syntax? Maybe "snap:foo/classic"? AFAICS "/" isn't
> used apart from in _filter_packages to specify a regex & that wouldn't apply
> here. For outputting I can imagine either passing that straight through,
> transforming it to "foo (classic)", or adding a new column in increasing order
> of difficulty on the livecd-rootfs side. Not that difficulty there matters too
> much.

OK, I did this. I went for the second option. Classic snaps should be annotated with "/classic", and that is transformed to "(classic)" in the first column of the output.

(In livecd-rootfs I translate that back to "/classic" to make it easier to pass to the function which installs snaps, which is slightly ugly but it seems worth it to me to have a more human-readable output from germinate.)

Revision history for this message
Iain Lane (laney) wrote :

gentle ping for a review please

Revision history for this message
Colin Watson (cjwatson) wrote :

I would not be very surprised to find extra requirements popping up here later that need changes to the syntax, but I guess this will do for now and we can see where the wind takes us. Thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/germinate/germinator.py b/germinate/germinator.py
2index a86c25d..310ad44 100644
3--- a/germinate/germinator.py
4+++ b/germinate/germinator.py
5@@ -128,6 +128,7 @@ class GerminatedSeed(object):
6 self._entries = []
7 self._features = set()
8 self._recommends_entries = []
9+ self._snaps = set()
10 self._close_seeds = set()
11 self._depends = set()
12 self._build_depends = set()
13@@ -139,6 +140,7 @@ class GerminatedSeed(object):
14 self._build_srcs = set()
15 self._not_build_srcs = set()
16 self._reasons = {}
17+ self._snap_reasons = {}
18 self._blacklist = set()
19 self._blacklist_seen = False
20 # Note that this relates to the vestigial global blacklist file, not
21@@ -194,6 +196,7 @@ class GerminatedSeed(object):
22 self._entries = copy._entries
23 self._recommends_entries = copy._recommends_entries
24 self._depends = copy._depends
25+ self._snaps = copy._snaps
26 self._build_depends = copy._build_depends
27 self._sourcepkgs = copy._sourcepkgs
28 self._build_sourcepkgs = copy._build_sourcepkgs
29@@ -202,6 +205,7 @@ class GerminatedSeed(object):
30 self._build_srcs = copy._build_srcs
31 self._not_build_srcs = copy._not_build_srcs
32 self._reasons = copy._reasons
33+ self._snap_reasons = copy._snap_reasons
34 self._blacklist_seen = False
35 self._blacklisted = copy._blacklisted
36 self._grown = True
37@@ -234,6 +238,10 @@ class GerminatedSeed(object):
38 return set(self._depends)
39
40 @property
41+ def snaps(self):
42+ return self._snaps
43+
44+ @property
45 def build_depends(self):
46 return set(self._build_depends)
47
48@@ -328,6 +336,7 @@ class GerminatedSeedStructure(object):
49 self._all = set()
50 self._all_srcs = set()
51 self._all_reasons = {}
52+ self._all_snap_reasons = {}
53
54 self._blacklist = {}
55
56@@ -765,6 +774,7 @@ class Germinator(object):
57
58 seedpkgs = []
59 seedrecommends = []
60+ seedsnaps = []
61 substvars = {}
62
63 for line in raw_seed:
64@@ -781,7 +791,7 @@ class Germinator(object):
65 pkg = pkg[:pkg.find("#")]
66
67 colon = pkg.find(":")
68- if colon != -1:
69+ if colon != -1 and not pkg.startswith("snap:"):
70 # Special header
71 name = pkg[:colon]
72 name = name.lower()
73@@ -846,10 +856,22 @@ class Germinator(object):
74 else:
75 is_blacklist = False
76
77+ # this is a seeded snap
78+ if pkg.startswith("snap:"):
79+ is_snap = True
80+ pkg = pkg[5:]
81+ if pkg.endswith("/classic"):
82+ pkg = "%s (classic)" % pkg[:-8]
83+ else:
84+ is_snap = False
85+
86 # a (pkgname) indicates that this is a recommend
87 # and not a depends
88 if pkg.startswith('(') and pkg.endswith(')'):
89 pkg = pkg[1:-1]
90+ if is_snap or pkg.startswith("snap:"):
91+ _logger.warning("Recommends entries cannot be used with snap packages, ignoring %s", pkg)
92+ continue
93 pkgs = self._filter_packages(self._packages, pkg)
94 if not pkgs:
95 pkgs = [pkg] # virtual or expanded; check again later
96@@ -857,6 +879,10 @@ class Germinator(object):
97 seedrecommends.extend(self._substitute_seed_vars(
98 substvars, pkg))
99
100+ if is_snap and pkg.startswith('%'):
101+ _logger.warning("%% entries cannot be used with snap packages, ignoring %s", pkg)
102+ continue
103+
104 if pkg.startswith('%'):
105 pkg = pkg[1:]
106 if pkg in self._sources:
107@@ -865,6 +891,8 @@ class Germinator(object):
108 else:
109 _logger.warning("Unknown source package: %s", pkg)
110 pkgs = []
111+ elif is_snap:
112+ pkgs = [pkg]
113 else:
114 pkgs = self._filter_packages(self._packages, pkg)
115 if not pkgs:
116@@ -876,8 +904,12 @@ class Germinator(object):
117 seed._blacklist.update(self._substitute_seed_vars(
118 substvars, pkg))
119 else:
120- for pkg in pkgs:
121- seedpkgs.extend(self._substitute_seed_vars(substvars, pkg))
122+ if is_snap:
123+ for pkg in pkgs:
124+ seedsnaps.extend(self._substitute_seed_vars(substvars, pkg))
125+ else:
126+ for pkg in pkgs:
127+ seedpkgs.extend(self._substitute_seed_vars(substvars, pkg))
128
129 di_kernel_versions = None
130
131@@ -921,6 +953,9 @@ class Germinator(object):
132 # No idea
133 _logger.error("Unknown %s package: %s", seed, pkg)
134
135+ for pkg in seedsnaps:
136+ seed._snaps.add(pkg)
137+
138 for pkg in self._hints:
139 if (self._hints[pkg] == seed.name and
140 not self._already_seeded(seed, pkg)):
141@@ -1023,6 +1058,10 @@ class Germinator(object):
142 self._di_kernel_versions = pkg
143 else:
144 self._add_package(seed, pkg, seed._seed_reason)
145+ for pkg in self.get_seed_snap_entries(structure, seedname):
146+ # avoid collisions with deb packages with the same name
147+ self._remember_why(seed._snap_reasons, pkg, seed._seed_reason, False, False)
148+ self._remember_why(output._all_snap_reasons, pkg, seed._seed_reason, False, False)
149
150 self._di_kernel_versions = None
151
152@@ -1736,6 +1775,23 @@ class Germinator(object):
153 if not isinstance(e, SeedKernelVersions)]
154 return ret
155
156+ def get_seed_snap_entries(self, structure, seedname):
157+ """Return the explicitly seeded snap entries for this seed."""
158+ seed = self._get_seed(structure, seedname)
159+ output = set(seed._snaps)
160+ for innerseed in self._inner_seeds(seed):
161+ if innerseed.name == seed.name:
162+ continue
163+ output -= innerseed._depends
164+ # Take care to preserve the original ordering.
165+ # Use a temporary variable to work around a pychecker bug.
166+ ret = [e for e in seed._snaps if e in output]
167+ return ret
168+
169+ def get_snaps(self, structure, seedname):
170+ """ Return the explicitly seeded snap entries for this seed."""
171+ return self._get_seed(structure, seedname).snaps
172+
173 def get_depends(self, structure, seedname):
174 """Return the dependencies of this seed."""
175 return self._get_seed(structure, seedname).depends
176@@ -1842,6 +1898,34 @@ class Germinator(object):
177 print(fmt % (src_len, src, mnt_len,
178 self._sources[src]["Maintainer"]), file=f)
179
180+ def _write_snap_list(self, reasons, filename, snapset):
181+ snaplist = sorted(snapset)
182+
183+ pkg_len = len("Package")
184+ why_len = len("Why")
185+
186+ for pkg in snaplist:
187+ _pkg_len = len(pkg)
188+ if _pkg_len > pkg_len:
189+ pkg_len = _pkg_len
190+
191+ why = reasons[pkg][0] if pkg in reasons else ""
192+ _why_len = len(str(why))
193+ if _why_len > why_len:
194+ why_len = _why_len
195+
196+ with AtomicFile(filename) as f:
197+ print("%-*s | %-*s" %
198+ (pkg_len, "Package",
199+ why_len, "Why"), file=f)
200+ print(("-" * pkg_len) + "-+-" + ("-" * why_len), file=f)
201+ for pkg in snaplist:
202+ why = reasons[pkg][0] if pkg in reasons else ""
203+ print("%-*s | %-*s" %
204+ (pkg_len, pkg,
205+ why_len, why), file=f)
206+ print(("-" * (pkg_len + why_len + 3)), file=f)
207+
208 def write_full_list(self, structure, filename, seedname):
209 """Write the full (run-time) dependency expansion of this seed."""
210 seed = self._get_seed(structure, seedname)
211@@ -1865,6 +1949,11 @@ class Germinator(object):
212 seed = self._get_seed(structure, seedname)
213 self._write_list(seed._reasons, filename, seed._depends)
214
215+ def write_snap_list(self, structure, filename, seedname):
216+ """Write the snaps seeded in this seed."""
217+ seed = self._get_seed(structure, seedname)
218+ self._write_snap_list(seed._snap_reasons, filename, seed._snaps)
219+
220 def write_build_depends_list(self, structure, filename, seedname):
221 """Write the build-dependencies of this seed."""
222 seed = self._get_seed(structure, seedname)
223@@ -1896,6 +1985,16 @@ class Germinator(object):
224 self._write_list(self._output[structure]._all_reasons, filename,
225 all_bins)
226
227+ def write_all_snap_list(self, structure, filename):
228+ """Write all the snap packages in this structure."""
229+ all_snaps = set()
230+
231+ for seedname in structure.names:
232+ all_snaps |= self.get_snaps(structure, seedname)
233+
234+ self._write_snap_list(self._output[structure]._all_snap_reasons, filename,
235+ all_snaps)
236+
237 def write_all_source_list(self, structure, filename):
238 """Write all the source packages for this structure."""
239 all_srcs = set()
240diff --git a/germinate/log.py b/germinate/log.py
241index dc7c0f2..558779f 100644
242--- a/germinate/log.py
243+++ b/germinate/log.py
244@@ -57,3 +57,4 @@ def germinate_logging(level):
245 handler.setFormatter(GerminateFormatter())
246 logger.addHandler(handler)
247 logger.propagate = False
248+ return logger
249diff --git a/germinate/scripts/germinate_main.py b/germinate/scripts/germinate_main.py
250index 094e27e..0b9f29d 100644
251--- a/germinate/scripts/germinate_main.py
252+++ b/germinate/scripts/germinate_main.py
253@@ -187,6 +187,7 @@ def main(argv):
254 g.write_depends_list(structure, seedname + ".depends", seedname)
255 g.write_build_depends_list(structure,
256 seedname + ".build-depends", seedname)
257+ g.write_snap_list (structure, seedname + ".snaps", seedname)
258
259 if seedname != "extra" and seedname in structure:
260 structure.write_seed_text(seedname + ".seedtext", seedname)
261@@ -196,6 +197,7 @@ def main(argv):
262
263 g.write_all_list(structure, "all")
264 g.write_all_source_list(structure, "all.sources")
265+ g.write_all_snap_list(structure, "all.snaps")
266
267 g.write_supported_list(structure, "%s+build-depends" % structure.supported)
268 g.write_supported_source_list(
269diff --git a/germinate/tests/helpers.py b/germinate/tests/helpers.py
270index b1ad38c..180ae48 100644
271--- a/germinate/tests/helpers.py
272+++ b/germinate/tests/helpers.py
273@@ -130,5 +130,12 @@ class TestCase(unittest.TestCase):
274 with io.open(seed_path, "a", encoding="UTF-8") as seed:
275 print(u(" * %s") % pkg, file=seed)
276
277+ def addSeedSnap(self, seed_dist, seed_name, pkg):
278+ self.setUpDirs()
279+ seed_path = os.path.join(self.seeds_dir, seed_dist, seed_name)
280+ self.ensureParentDir(seed_path)
281+ with io.open(seed_path, "a", encoding="UTF-8") as seed:
282+ print(u(" * snap:%s") % pkg, file=seed)
283+
284 def openSeedStructure(self, branch):
285 return SeedStructure(branch, seed_bases=["file://%s" % self.seeds_dir])
286diff --git a/germinate/tests/test_germinator.py b/germinate/tests/test_germinator.py
287index 79956dc..2cb0d52 100644
288--- a/germinate/tests/test_germinator.py
289+++ b/germinate/tests/test_germinator.py
290@@ -358,4 +358,38 @@ class TestGerminator(TestCase):
291 set(["gettext", "debhelper"]),
292 germinator.get_build_depends(structure, "base"))
293
294+ def test_snap(self):
295+ import logging
296+ from germinate.log import germinate_logging
297+ germinate_logging(logging.INFO)
298+ branch = "collection.precise"
299+ self.addSeed(branch, "base")
300+ self.addSeedSnap(branch, "base", "hello")
301+ germinator = Germinator("i386")
302+ structure = self.openSeedStructure(branch)
303+ germinator.plant_seeds(structure)
304+ germinator.grow(structure)
305+
306+ self.assertEqual(
307+ set(["hello"]),
308+ germinator.get_snaps(structure, "base"))
309+
310+ def test_snap_recommends(self):
311+ import logging
312+ from germinate.log import germinate_logging
313+ logger = germinate_logging(logging.INFO)
314+ branch = "collection.precise"
315+ self.addSeed(branch, "base")
316+ self.addSeedSnap(branch, "base", "(hello)")
317+ with self.assertLogs(logger, level=logging.WARNING) as logs:
318+ germinator = Germinator("i386")
319+ structure = self.openSeedStructure(branch)
320+ germinator.plant_seeds(structure)
321+ germinator.grow(structure)
322+ self.assertIn('ignoring hello', logs.output[0])
323+
324+ self.assertEqual(
325+ set([]),
326+ germinator.get_snaps(structure, "base"))
327+
328 # TODO: Germinator needs many more unit tests.
329diff --git a/germinate/tests/test_integration.py b/germinate/tests/test_integration.py
330index 6e16802..756f077 100644
331--- a/germinate/tests/test_integration.py
332+++ b/germinate/tests/test_integration.py
333@@ -67,6 +67,44 @@ class TestGerminate(TestCase):
334 self.assertTrue("hello" in supported)
335 self.assertTrue("hello-dependency" in supported)
336
337- all_ = self.parseOutput("supported")
338+ all_ = self.parseOutput("all")
339 self.assertTrue("hello" in all_)
340 self.assertTrue("hello-dependency" in all_)
341+
342+ def test_snap(self):
343+ # Need Packages
344+ self.addSource("warty", "main", "hello", "1.0-1",
345+ ["hello", "hello-dependency"])
346+ self.addPackage("warty", "main", "i386", "hello-dependency", "1.0-1",
347+ fields={"Source": "hello"})
348+ self.addSeed("ubuntu.warty", "supported")
349+ self.addSeedSnap("ubuntu.warty", "supported", "mycoolsnap")
350+ self.runGerminate("-s", "ubuntu.warty", "-d", "warty", "-c", "main")
351+
352+ supported = self.parseOutput("supported.snaps")
353+ self.assertIn("mycoolsnap", supported)
354+
355+ all_ = self.parseOutput("all.snaps")
356+ self.assertIn("mycoolsnap", all_)
357+
358+ debs = self.parseOutput("supported")
359+ self.assertNotIn("mycoolsnap", debs)
360+
361+ def test_classic_snap(self):
362+ # Need Packages
363+ self.addSource("warty", "main", "hello", "1.0-1",
364+ ["hello", "hello-dependency"])
365+ self.addPackage("warty", "main", "i386", "hello-dependency", "1.0-1",
366+ fields={"Source": "hello"})
367+ self.addSeed("ubuntu.warty", "supported")
368+ self.addSeedSnap("ubuntu.warty", "supported", "mycoolsnap/classic")
369+ self.runGerminate("-s", "ubuntu.warty", "-d", "warty", "-c", "main")
370+
371+ supported = self.parseOutput("supported.snaps")
372+ self.assertIn("mycoolsnap (classic)", supported)
373+
374+ all_ = self.parseOutput("all.snaps")
375+ self.assertIn("mycoolsnap (classic)", all_)
376+
377+ debs = self.parseOutput("supported")
378+ self.assertNotIn("mycoolsnap (classic)", debs)
379diff --git a/germinate/tests/test_seeds.py b/germinate/tests/test_seeds.py
380index f7815ba..cc81b3b 100644
381--- a/germinate/tests/test_seeds.py
382+++ b/germinate/tests/test_seeds.py
383@@ -55,6 +55,7 @@ class TestSeed(TestCase):
384 self.addSeedPackage("collection.dist", "test2", "foo")
385 self.addSeed("collection.dist", "test3")
386 self.addSeedPackage("collection.dist", "test3", "bar")
387+ self.addSeedSnap("collection.dist", "test3", "quux")
388
389 def test_init_no_vcs(self):
390 """__init__ can open a seed from a collection without a VCS."""
391@@ -65,6 +66,15 @@ class TestSeed(TestCase):
392 self.assertEqual("collection.dist", seed.branch)
393 self.assertEqual(" * foo\n", seed.text)
394
395+ def test_init_no_vcs_snap(self):
396+ """__init__ can open a seed from a collection without a VCS."""
397+ seed = Seed(
398+ ["file://%s" % self.seeds_dir], ["collection.dist"], "test3")
399+ self.assertEqual("test3", seed.name)
400+ self.assertEqual("file://%s" % self.seeds_dir, seed.base)
401+ self.assertEqual("collection.dist", seed.branch)
402+ self.assertEqual(" * bar\n * snap:quux\n", seed.text)
403+
404 def test_behaves_as_file(self):
405 """A Seed context can be read from as a file object."""
406 seed = Seed(
407diff --git a/man/germinate.1 b/man/germinate.1
408index 1b8edf3..68682f2 100644
409--- a/man/germinate.1
410+++ b/man/germinate.1
411@@ -108,6 +108,27 @@ It is not intended for the purpose of working around buggy package
412 relationships, and attempts to do so will not work because
413 .Ic apt
414 has no way to know about blacklist entries in seeds.
415+.It snap:name
416+Seed entries beginning with
417+.Sq \&snap:
418+are
419+.Em snap
420+packages. These are different from
421+.Em deb
422+packages in that they do not have (build-)dependencies, can not be
423+recommended, and do not end up in any resulting metapackages. (If you try to
424+recommend a snap package, it will be ignored completely.) Snaps specified in
425+seeds will be output in a
426+.Em .snaps
427+file named after the corresponding seed, as software processing the output of
428+.Nm
429+will typically need to treat snaps differently from debs.
430+.Nm
431+will not check remotely to see if a given snap is available, therefore seeds
432+are expected to explicitly list all architectures a snap is to be seeded on.
433+.Sq \&snap:
434+entries can also be suffixed with "/classic" to indicate that the snaps need to
435+be installed with classic confinement on end-user systems.
436 .It key: value
437 Some seeds also contain headers at the top of the file, in
438 .Dq key: value

Subscribers

People subscribed via source and target branches

to all changes: