← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~ruinedyourlife/launchpad:craft-build-rust-scan into launchpad:master

 

Quentin Debhi has proposed merging ~ruinedyourlife/launchpad:craft-build-rust-scan into launchpad:master.

Commit message:
Scan crates inside rust build archives

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~ruinedyourlife/launchpad/+git/launchpad/+merge/477899

This will find `.crate` files inside `sourcecraft` outputted archives and add it as the build output.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~ruinedyourlife/launchpad:craft-build-rust-scan into launchpad:master.
diff --git a/lib/lp/archivepublisher/artifactory.py b/lib/lp/archivepublisher/artifactory.py
index 08eb83a..e435aa7 100644
--- a/lib/lp/archivepublisher/artifactory.py
+++ b/lib/lp/archivepublisher/artifactory.py
@@ -617,7 +617,7 @@ class ArtifactoryPool:
             ]
         elif repository_format == ArchiveRepositoryFormat.RUST:
             return [
-                "*.tar.xz",
+                "*.crate",
             ]
         elif repository_format == ArchiveRepositoryFormat.GENERIC:
             return ["*"]
diff --git a/lib/lp/archivepublisher/tests/test_artifactory.py b/lib/lp/archivepublisher/tests/test_artifactory.py
index 3fc2aa5..a5212b7 100644
--- a/lib/lp/archivepublisher/tests/test_artifactory.py
+++ b/lib/lp/archivepublisher/tests/test_artifactory.py
@@ -290,7 +290,7 @@ class TestArtifactoryPool(TestCase):
     def test_getArtifactPatterns_rust(self):
         pool = self.makePool()
         self.assertEqual(
-            ["*.tar.xz"],
+            ["*.crate"],
             pool.getArtifactPatterns(ArchiveRepositoryFormat.RUST),
         )
 
diff --git a/lib/lp/archiveuploader/craftrecipeupload.py b/lib/lp/archiveuploader/craftrecipeupload.py
index 5e9d285..a7ed62f 100644
--- a/lib/lp/archiveuploader/craftrecipeupload.py
+++ b/lib/lp/archiveuploader/craftrecipeupload.py
@@ -8,7 +8,10 @@ __all__ = [
 ]
 
 import os
+import tarfile
+import tempfile
 
+import yaml
 from zope.component import getUtility
 
 from lp.archiveuploader.utils import UploadError
@@ -45,6 +48,7 @@ class CraftRecipeUpload:
                 # All relevant files will be in a subdirectory.
                 continue
             for craft_file in sorted(filenames):
+                # Look for .tar.xz archives (which might contain .crate files)
                 if craft_file.endswith(".tar.xz"):
                     found_craft = True
                 craft_paths.append(os.path.join(dirpath, craft_file))
@@ -53,18 +57,52 @@ class CraftRecipeUpload:
             raise UploadError("Build did not produce any craft files.")
 
         for craft_path in craft_paths:
-            with open(craft_path, "rb") as file:
-                libraryfile = self.librarian.create(
-                    os.path.basename(craft_path),
-                    os.stat(craft_path).st_size,
-                    file,
-                    filenameToContentType(craft_path),
-                    restricted=build.is_private,
-                )
-            build.addFile(libraryfile)
+            # Extract and process .crate files from archive
+            self._process_rust_archive(build, craft_path)
 
         # The master verifies the status to confirm successful upload.
         self.logger.debug("Updating %s" % build.title)
         build.updateStatus(BuildStatus.FULLYBUILT)
-
         self.logger.debug("Finished upload.")
+
+    def _process_rust_archive(self, build, archive_path):
+        """Process a .tar.xz archive that may contain .crate files."""
+        with tempfile.TemporaryDirectory() as tmpdir:
+            with tarfile.open(archive_path, "r:xz") as tar:
+                tar.extractall(path=tmpdir)
+
+            # Read metadata.yaml for crate info
+            # XXX: ruinedyourlife 2024-12-06
+            # We will need this later to give the crate a name and version to
+            # artifactory. This is a placeholder for now, which will be changed
+            # when we find a way to send that information to artifactory.
+            metadata_path = os.path.join(tmpdir, "metadata.yaml")
+            if os.path.exists(metadata_path):
+                with open(metadata_path) as f:
+                    try:
+                        metadata = yaml.safe_load(f)
+                        _ = metadata.get("name")
+                        _ = metadata.get("version")
+                    except Exception as e:
+                        self.logger.warning(
+                            "Failed to parse metadata.yaml: %s", e
+                        )
+
+            # Look for .crate files in extracted contents
+            for root, _, files in os.walk(tmpdir):
+                for filename in files:
+                    if filename.endswith(".crate"):
+                        crate_path = os.path.join(root, filename)
+                        self._process_crate_file(build, crate_path)
+
+    def _process_crate_file(self, build, crate_path):
+        """Process a single .crate file."""
+        with open(crate_path, "rb") as file:
+            libraryfile = self.librarian.create(
+                os.path.basename(crate_path),
+                os.stat(crate_path).st_size,
+                file,
+                filenameToContentType(crate_path),
+                restricted=build.is_private,
+            )
+        build.addFile(libraryfile)
diff --git a/lib/lp/archiveuploader/tests/test_craftrecipeupload.py b/lib/lp/archiveuploader/tests/test_craftrecipeupload.py
index 5f7f737..c137c78 100644
--- a/lib/lp/archiveuploader/tests/test_craftrecipeupload.py
+++ b/lib/lp/archiveuploader/tests/test_craftrecipeupload.py
@@ -4,8 +4,12 @@
 """Tests for `CraftRecipeUpload`."""
 
 import os
+import tarfile
+import tempfile
 
+import yaml
 from storm.store import Store
+from zope.security.proxy import removeSecurityProxy
 
 from lp.archiveuploader.tests.test_uploadprocessor import (
     TestUploadProcessorBase,
@@ -39,16 +43,45 @@ class TestCraftRecipeUploads(TestUploadProcessorBase):
             self.layer.txn, builds=True
         )
 
+    def _createArchiveWithCrate(
+        self, upload_dir, crate_name="test-crate", crate_version="0.1.0"
+    ):
+        """Helper to create a tar.xz archive containing a crate & metadata."""
+        # Create a temporary directory to build our archive
+        with tempfile.TemporaryDirectory() as tmpdir:
+            # Create metadata.yaml
+            metadata = {
+                "name": crate_name,
+                "version": crate_version,
+            }
+            metadata_path = os.path.join(tmpdir, "metadata.yaml")
+            with open(metadata_path, "w") as f:
+                yaml.safe_dump(metadata, f)
+
+            # Create dummy crate file
+            crate_path = os.path.join(
+                tmpdir, f"{crate_name}-{crate_version}.crate"
+            )
+            with open(crate_path, "wb") as f:
+                f.write(b"dummy crate contents")
+
+            # Create tar.xz archive
+            archive_path = os.path.join(upload_dir, "output.tar.xz")
+            with tarfile.open(archive_path, "w:xz") as tar:
+                tar.add(metadata_path, arcname="metadata.yaml")
+                tar.add(crate_path, arcname=os.path.basename(crate_path))
+
+            return archive_path
+
     def test_sets_build_and_state(self):
         # The upload processor uploads files and sets the correct status.
         self.assertFalse(self.build.verifySuccessfulUpload())
         upload_dir = os.path.join(
             self.incoming_folder, "test", str(self.build.id), "ubuntu"
         )
-        write_file(
-            os.path.join(upload_dir, "foo_0_all.craft.tar.xz"), b"craft"
-        )
-        write_file(os.path.join(upload_dir, "foo_0_all.manifest"), b"manifest")
+        os.makedirs(upload_dir, exist_ok=True)
+        self._createArchiveWithCrate(upload_dir)
+
         handler = UploadHandler.forProcessor(
             self.uploadprocessor, self.incoming_folder, "test", self.build
         )
@@ -61,6 +94,42 @@ class TestCraftRecipeUploads(TestUploadProcessorBase):
         self.assertEqual(BuildStatus.FULLYBUILT, self.build.status)
         self.assertTrue(self.build.verifySuccessfulUpload())
 
+        # Verify that the crate file was properly extracted and stored
+        build = removeSecurityProxy(self.build)
+        files = list(build.getFiles())
+        self.assertEqual(1, len(files))
+        stored_file = files[0][1]
+        self.assertTrue(stored_file.filename.endswith(".crate"))
+
+    def test_processes_crate_from_archive(self):
+        """Test extracting/processing crates within .tar.xz archives."""
+        upload_dir = os.path.join(
+            self.incoming_folder, "test", str(self.build.id), "ubuntu"
+        )
+        os.makedirs(upload_dir, exist_ok=True)
+
+        # Create archive with specific crate name and version
+        crate_name = "test-crate"
+        crate_version = "0.2.0"
+        self._createArchiveWithCrate(upload_dir, crate_name, crate_version)
+
+        handler = UploadHandler.forProcessor(
+            self.uploadprocessor, self.incoming_folder, "test", self.build
+        )
+        result = handler.processCraftRecipe(self.log)
+
+        # Verify upload succeeded
+        self.assertEqual(UploadStatusEnum.ACCEPTED, result)
+        self.assertEqual(BuildStatus.FULLYBUILT, self.build.status)
+
+        # Verify the crate file was properly stored
+        build = removeSecurityProxy(self.build)
+        files = list(build.getFiles())
+        self.assertEqual(1, len(files))
+        stored_file = files[0][1]
+        expected_filename = f"{crate_name}-{crate_version}.crate"
+        self.assertEqual(expected_filename, stored_file.filename)
+
     def test_requires_craft(self):
         # The upload processor fails if the upload does not contain any
         # .craft files.
diff --git a/lib/lp/soyuz/model/archivejob.py b/lib/lp/soyuz/model/archivejob.py
index 100e3ba..65d2861 100644
--- a/lib/lp/soyuz/model/archivejob.py
+++ b/lib/lp/soyuz/model/archivejob.py
@@ -330,6 +330,9 @@ class CIBuildUploadJob(ArchiveJobDerived):
             SourcePackageFileType.GO_MODULE_MOD,
             SourcePackageFileType.GO_MODULE_ZIP,
         },
+        # XXX: ruinedyourlife 2024-12-06
+        # Remove the Rust format and it's scanner as we don't need it from
+        # CI builds. We only care about crates in craft recipe uploads.
         ArchiveRepositoryFormat.RUST: {
             BinaryPackageFormat.CRATE,
         },