Repository: canonical/snapcraft

Issue:
machine information
[design](https://forum.snapcraft.io/t/record-machine-information-in-the-manifest/1961)


Discussion:

Resolving patch:
diff --git a/snapcraft/internal/pluginhandler/__init__.py b/snapcraft/internal/pluginhandler/__init__.py
--- a/snapcraft/internal/pluginhandler/__init__.py
+++ b/snapcraft/internal/pluginhandler/__init__.py
@@ -342,7 +342,10 @@ def mark_build_done(self):
             plugin_manifest, machine_manifest))
 
     def _get_machine_manifest(self):
-        return {'uname': common.run_output(['uname', '-srvmpio'])}
+        return {
+            'uname': common.run_output(['uname', '-srvmpio']),
+            'installed-packages': repo.Repo.get_installed_packages()
+        }
 
     def clean_build(self, hint=''):
         if self.is_clean('build'):
diff --git a/snapcraft/internal/repo/_base.py b/snapcraft/internal/repo/_base.py
--- a/snapcraft/internal/repo/_base.py
+++ b/snapcraft/internal/repo/_base.py
@@ -119,6 +119,14 @@ def is_package_installed(cls, package_name):
         """
         raise NotImplementedError()
 
+    @classmethod
+    def get_installed_packages(cls):
+        """Return a list of the installed packages and their versions
+
+        :rtype: list of strings with the form package=version.
+        """
+        raise NotImplementedError()
+
     def __init__(self, rootdir, *args, **kwargs):
         """Initialize a repository handler.
 
diff --git a/snapcraft/internal/repo/_deb.py b/snapcraft/internal/repo/_deb.py
--- a/snapcraft/internal/repo/_deb.py
+++ b/snapcraft/internal/repo/_deb.py
@@ -260,6 +260,17 @@ def is_package_installed(cls, package_name):
         with apt.Cache() as apt_cache:
             return apt_cache[package_name].installed
 
+    @classmethod
+    def get_installed_packages(cls):
+        installed_packages = []
+        with apt.Cache() as apt_cache:
+            for package in apt_cache:
+                if package.installed:
+                    installed_packages.append(
+                        '{}={}'.format(
+                            package.name, package.installed.version))
+        return installed_packages
+
     def __init__(self, rootdir, sources=None, project_options=None):
         super().__init__(rootdir)
         self._downloaddir = os.path.join(rootdir, 'download')


Test patch:
diff --git a/integration_tests/test_asset_recording.py b/integration_tests/test_asset_recording.py
--- a/integration_tests/test_asset_recording.py
+++ b/integration_tests/test_asset_recording.py
@@ -20,9 +20,10 @@
 import sys
 import yaml
 
+import apt
 import fixtures
 import testscenarios
-from testtools.matchers import Equals
+from testtools.matchers import Contains, Equals
 
 import snapcraft
 import integration_tests
@@ -70,6 +71,21 @@ def test_prime_records_uname(self):
             recorded_yaml['parts']['dummy-part']['uname'],
             Equals(expected_uname))
 
+    def test_prime_records_installed_packages(self):
+        self.run_snapcraft('prime', project_dir='basic')
+
+        recorded_yaml_path = os.path.join(
+            self.prime_dir, 'snap', 'manifest.yaml')
+        with open(recorded_yaml_path) as recorded_yaml_file:
+            recorded_yaml = yaml.load(recorded_yaml_file)
+
+        with apt.Cache() as apt_cache:
+            expected_package = 'python3={}'.format(
+                apt_cache['python3'].installed.version)
+        self.assertThat(
+            recorded_yaml['parts']['dummy-part']['installed-packages'],
+            Contains(expected_package))
+
     def test_prime_with_architectures(self):
         """Test the recorded manifest for a basic snap
 
diff --git a/snapcraft/tests/fixture_setup.py b/snapcraft/tests/fixture_setup.py
--- a/snapcraft/tests/fixture_setup.py
+++ b/snapcraft/tests/fixture_setup.py
@@ -843,8 +843,7 @@ def setUp(self):
         self.cache = self.Cache()
         self.mock_apt_cache.return_value = self.cache
         for package, version in self.packages:
-            self.cache[package] = FakeAptCachePackage(
-                self.path, package, version)
+            self.add_package(FakeAptCachePackage(package, version))
 
         # Add all the packages in the manifest.
         with open(os.path.abspath(
@@ -853,16 +852,20 @@ def setUp(self):
                     'internal', 'repo', 'manifest.txt'))) as manifest_file:
             self.add_packages([line.strip() for line in manifest_file])
 
+    def add_package(self, package):
+        package.temp_dir = self.path
+        self.cache[package.name] = package
+
     def add_packages(self, package_names):
         for name in package_names:
-            self.cache[name] = FakeAptCachePackage(self.path, name)
+            self.cache[name] = FakeAptCachePackage(name)
 
 
 class FakeAptCachePackage():
 
     def __init__(
-            self, temp_dir, name, version=None,
-            provides=None, installed=False,
+            self, name, version=None, installed=None,
+            temp_dir=None, provides=None,
             priority='non-essential'):
         super().__init__()
         self.temp_dir = temp_dir
@@ -871,9 +874,14 @@ def __init__(
         self.versions = {}
         self.version = version
         self.candidate = self
-        self.installed = version
         self.provides = provides if provides else []
-        self.installed = installed
+        if installed:
+            # XXX The installed attribute requires some values that the fake
+            # package also requires. The shortest path to do it that I found
+            # was to get installed to return the same fake package.
+            self.installed = self
+        else:
+            self.installed = None
         self.priority = priority
         self.marked_install = False
 
diff --git a/snapcraft/tests/repo/test_deb.py b/snapcraft/tests/repo/test_deb.py
--- a/snapcraft/tests/repo/test_deb.py
+++ b/snapcraft/tests/repo/test_deb.py
@@ -223,6 +223,27 @@ def test_sources_armhf_trusty(self, mock_cc):
         self.assertFalse(mock_cc.called)
 
 
+class UbuntuTestCaseWithFakeAptCache(RepoBaseTestCase):
+
+    def setUp(self):
+        super().setUp()
+        self.fake_apt_cache = fixture_setup.FakeAptCache()
+        self.useFixture(self.fake_apt_cache)
+
+    def test_get_installed_packages(self):
+        for name, version, installed in (
+                ('test-installed-package', 'test-installed-package-version',
+                 True),
+                ('test-not-installed-package', 'dummy', False)):
+            self.fake_apt_cache.add_package(
+                fixture_setup.FakeAptCachePackage(
+                    name, version, installed=installed))
+
+        self.assertThat(
+            repo.Repo.get_installed_packages(),
+            Equals(['test-installed-package=test-installed-package-version']))
+
+
 class BuildPackagesTestCase(tests.TestCase):
 
     def setUp(self):
diff --git a/snapcraft/tests/test_lifecycle.py b/snapcraft/tests/test_lifecycle.py
--- a/snapcraft/tests/test_lifecycle.py
+++ b/snapcraft/tests/test_lifecycle.py
@@ -719,6 +719,9 @@ def fake_uname(cmd, *args, **kwargs):
         check_output_patcher.start()
         self.addCleanup(check_output_patcher.stop)
 
+        self.fake_apt_cache = fixture_setup.FakeAptCache()
+        self.useFixture(self.fake_apt_cache)
+
 
 class RecordManifestTestCase(RecordManifestBaseTestCase):
 
@@ -740,6 +743,7 @@ def test_prime_with_build_info_records_manifest(self):
 parts:
   test-part:
     build-packages: []
+    installed-packages: []
     plugin: nil
     prime: []
     stage: []
@@ -752,12 +756,51 @@ def test_prime_with_build_info_records_manifest(self):
             os.path.join('prime', 'snap', 'manifest.yaml'),
             FileContains(expected))
 
+    def test_prime_with_installed_packages(self):
+        self.useFixture(fixtures.EnvironmentVariable(
+            'SNAPCRAFT_BUILD_INFO', '1'))
+        for name, version in [('test-package1', 'test-version1'),
+                              ('test-package2', 'test-version2')]:
+            self.fake_apt_cache.add_package(
+                fixture_setup.FakeAptCachePackage(
+                    name, version, installed=True))
+
+        self.make_snapcraft_yaml("""parts:
+  test-part:
+    plugin: nil
+""")
+        lifecycle.execute('prime', self.project_options)
+
+        expected = ("""name: test
+version: 0
+summary: test
+description: test
+confinement: strict
+grade: stable
+parts:
+  test-part:
+    build-packages: []
+    installed-packages: {}
+    plugin: nil
+    prime: []
+    stage: []
+    stage-packages: []
+    uname: Linux test uname 4.10 x86_64
+architectures: [{}]
+build-packages: []
+""".format('[test-package1=test-version1, test-package2=test-version2]',
+           self.project_options.deb_arch))
+        self.assertThat(
+            os.path.join('prime', 'snap', 'manifest.yaml'),
+            FileContains(expected))
+
     def test_prime_with_stage_packages(self):
         self.useFixture(fixtures.EnvironmentVariable(
             'SNAPCRAFT_BUILD_INFO', '1'))
-        self.useFixture(fixture_setup.FakeAptCache([
-            ('test-package1', 'test-version1'),
-            ('test-package2', 'test-version2')]))
+        for name, version in [('test-package1', 'test-version1'),
+                              ('test-package2', 'test-version2')]:
+            self.fake_apt_cache.add_package(
+                fixture_setup.FakeAptCachePackage(name, version))
 
         self.make_snapcraft_yaml("""parts:
   test-part:
@@ -776,6 +819,7 @@ def test_prime_with_stage_packages(self):
 parts:
   test-part:
     build-packages: []
+    installed-packages: []
     plugin: nil
     prime: []
     stage: []
@@ -792,9 +836,10 @@ def test_prime_with_stage_packages(self):
     def test_prime_with_global_build_packages(self, _):
         self.useFixture(fixtures.EnvironmentVariable(
             'SNAPCRAFT_BUILD_INFO', '1'))
-        self.useFixture(fixture_setup.FakeAptCache([
-            ('test-package1', 'test-version1'),
-            ('test-package2', 'test-version2')]))
+        for name, version in [('test-package1', 'test-version1'),
+                              ('test-package2', 'test-version2')]:
+            self.fake_apt_cache.add_package(
+                fixture_setup.FakeAptCachePackage(name, version))
 
         self.make_snapcraft_yaml("""build-packages: [test-package1=test-version1, test-package2]
 parts:
@@ -814,6 +859,7 @@ def test_prime_with_global_build_packages(self, _):
 parts:
   test-part:
     build-packages: []
+    installed-packages: []
     plugin: nil
     prime: []
     stage: []
@@ -829,6 +875,8 @@ def test_prime_with_global_build_packages(self, _):
     def test_prime_with_source_details(self, _):
         self.useFixture(fixtures.EnvironmentVariable(
             'SNAPCRAFT_BUILD_INFO', '1'))
+        self.fake_apt_cache.add_package(
+            fixture_setup.FakeAptCachePackage('git', 'testversion'))
 
         self.make_snapcraft_yaml("""parts:
   test-part:
@@ -849,6 +897,7 @@ def test_prime_with_source_details(self, _):
 parts:
   test-part:
     build-packages: []
+    installed-packages: []
     plugin: nil
     prime: []
     source: test-source
@@ -861,7 +910,7 @@ def test_prime_with_source_details(self, _):
     stage-packages: []
     uname: Linux test uname 4.10 x86_64
 architectures: [{}]
-build-packages: []
+build-packages: [git=testversion]
 """.format(self.project_options.deb_arch))
         self.assertThat(
             os.path.join('prime', 'snap', 'manifest.yaml'),
@@ -871,8 +920,8 @@ def test_prime_with_source_details(self, _):
     def test_prime_with_build_package_with_any_architecture(self, _):
         self.useFixture(fixtures.EnvironmentVariable(
             'SNAPCRAFT_BUILD_INFO', '1'))
-        self.useFixture(fixture_setup.FakeAptCache([
-            ('test-package', 'test-version')]))
+        self.fake_apt_cache.add_package(fixture_setup.FakeAptCachePackage(
+            'test-package', 'test-version'))
 
         self.make_snapcraft_yaml("""parts:
   test-part:
@@ -891,6 +940,7 @@ def test_prime_with_build_package_with_any_architecture(self, _):
 parts:
   test-part:
     build-packages: ['test-package:any']
+    installed-packages: []
     plugin: nil
     prime: []
     stage: []
@@ -907,11 +957,8 @@ def test_prime_with_build_package_with_any_architecture(self, _):
     def test_prime_with_virtual_build_package(self, _):
         self.useFixture(fixtures.EnvironmentVariable(
             'SNAPCRAFT_BUILD_INFO', '1'))
-        fake_apt_cache = fixture_setup.FakeAptCache()
-        self.useFixture(fake_apt_cache)
-        fake_apt_cache.cache['test-provider-package'] = (
+        self.fake_apt_cache.add_package(
             fixture_setup.FakeAptCachePackage(
-                fake_apt_cache.path,
                 'test-provider-package', 'test-version',
                 provides=['test-virtual-package']))
 
@@ -932,6 +979,7 @@ def test_prime_with_virtual_build_package(self, _):
 parts:
   test-part:
     build-packages: [test-virtual-package]
+    installed-packages: []
     plugin: nil
     prime: []
     stage: []
@@ -965,6 +1013,7 @@ def test_prime_with_plugin_manifest(self, fake_plugin_manifest):
 parts:
   test-part:
     build-packages: []
+    installed-packages: []
     plugin: nil
     prime: []
     stage: []
@@ -1007,6 +1056,7 @@ def test_prime_step_records_prime_keyword(self):
 parts:
   test-part:
     build-packages: []
+    installed-packages: []
     plugin: nil
     prime: [-*]
     stage: []