Repository: gabrieldemarmiesse/python-on-whales (language: python)

Issue:
No support for buildx build --build-context ...
## Background
https://github.com/docker/buildx/releases/tag/v0.8.0

Build command now accepts --build-context flag to define additional named build contextes for your builds. For example, these can be used expose additional local directories to your build or pin an image to a specific digest. This feature requires using Dockerfile 1.4+ frontend. https://github.com/docker/buildx/pull/904 [Documentation](https://github.com/docker/buildx/blob/v0.8.0/docs/reference/buildx_build.md#build-context)

## Changes
Add --build-context arg for BuildxCLI

```
def build(
        self,
        ...
        build_contexts: Dict[str, str] = {},
    )...
...
full_cmd.add_args_list("--build-context", format_dict_for_cli(build_contexts))
```


Discussion / hints:
Thanks for creating the issue. This is a cool feature and a welcomed one. If anyone wants to make a pull request, I'll gladly review it :) 

Fix patch:
diff --git a/python_on_whales/components/buildx/cli_wrapper.py b/python_on_whales/components/buildx/cli_wrapper.py
index c1c2de5d..f73494b7 100644
--- a/python_on_whales/components/buildx/cli_wrapper.py
+++ b/python_on_whales/components/buildx/cli_wrapper.py
@@ -208,7 +208,7 @@ def build(
         allow: List[str] = [],
         attest: Optional[Dict[str, str]] = None,
         build_args: Dict[str, str] = {},
-        # TODO: build_context
+        build_contexts: Dict[str, Union[str, ValidPath]] = {},
         builder: Optional[ValidBuilder] = None,
         cache: bool = True,
         # TODO: cache_filters
@@ -254,6 +254,11 @@ def build(
             attest: Attestation parameters. Eg `attest={"type": "sbom", "generator": "my_image"}`
             build_args: The build arguments.
                 ex `build_args={"PY_VERSION": "3.7.8", "UBUNTU_VERSION": "20.04"}`.
+            build_contexts: Additional build contexts.
+                `build_contexts={[name]: [value], ...}`
+                Supports local directories, git repositories, HTTP URL to a tarball, a docker
+                image defined with a `docker-image://` prefix, and the `oci-layout://` protocol.
+                ex `build_contexts={"project2": "../path/to/project2/src", "qumu-src": "https://github.com/qemu/qemu.git"}`.
             builder: Specify which builder to use.
             cache: Whether or not to use the cache
             cache_from: Works only with the container driver. Loads the cache
@@ -317,6 +322,7 @@ def build(
         if isinstance(attest, dict):
             full_cmd.add_simple_arg("--attest", format_dict_for_buildx(attest))
         full_cmd.add_args_list("--build-arg", format_dict_for_cli(build_args))
+        full_cmd.add_args_list("--build-context", format_dict_for_cli(build_contexts))
         full_cmd.add_simple_arg("--builder", builder)
         full_cmd.add_args_list("--label", format_dict_for_cli(labels))
 


Test patch:
diff --git a/tests/python_on_whales/components/buildx/test_buildx_cli_wrapper.py b/tests/python_on_whales/components/buildx/test_buildx_cli_wrapper.py
index baec806f..9112ea40 100644
--- a/tests/python_on_whales/components/buildx/test_buildx_cli_wrapper.py
+++ b/tests/python_on_whales/components/buildx/test_buildx_cli_wrapper.py
@@ -1,4 +1,5 @@
 import os
+import tarfile
 
 import pytest
 
@@ -13,6 +14,21 @@
 RUN touch /dada
 """
 
+dockerfile_content2 = """
+FROM busybox
+COPY --from=test_context README.md /README.md
+"""
+
+dockerfile_content3 = """
+FROM test_context
+RUN touch /dada
+"""
+
+dockerfile_content4 = """
+FROM busybox
+COPY --from=test_context /dada /dada
+"""
+
 
 @pytest.fixture
 def with_docker_driver():
@@ -30,6 +46,24 @@ def with_container_driver():
     docker.buildx.use(current_builder)
 
 
+@pytest.fixture
+def with_oci_layout_compliant_dir(tmp_path):
+    (tmp_path / "Dockerfile").write_text(dockerfile_content1)
+
+    # Build the oci layout compliant directory
+    tar_path = os.path.join(tmp_path, "oci-layout.tar")
+    oci_folder_path = os.path.join(tmp_path, "oci-layout")
+
+    current_builder = docker.buildx.inspect()
+    with docker.buildx.create(use=True):
+        docker.buildx.build(tmp_path, output={"type": "oci", "dest": tar_path})
+    docker.buildx.use(current_builder)
+
+    # Extract tar to directory
+    with tarfile.open(tar_path) as tar:
+        tar.extractall(oci_folder_path)
+
+
 @pytest.mark.usefixtures("with_docker_driver")
 def test_buildx_build(tmp_path):
     (tmp_path / "Dockerfile").write_text(dockerfile_content1)
@@ -251,6 +285,63 @@ def test_buildx_build_attestations(tmp_path, kwargs):
     docker.buildx.build(tmp_path, **kwargs)
 
 
+# Does the build work when passing extra contexts
+# without making use of them in the Dockerfile
+@pytest.mark.usefixtures("with_container_driver")
+def test_buildx_build_build_context1(tmp_path):
+    (tmp_path / "Dockerfile").write_text(dockerfile_content1)
+    docker.buildx.build(tmp_path, build_contexts=dict(test_context="."))
+
+
+# Does the build work when passing extra contexts
+# when the Dockerfile does make use of them
+@pytest.mark.usefixtures("with_container_driver")
+@pytest.mark.parametrize(
+    "test_context",
+    [
+        # Test with local directory
+        PROJECT_ROOT,
+        # Test with git repo
+        "https://github.com/gabrieldemarmiesse/python-on-whales.git",
+    ],
+)
+def test_buildx_build_build_context2(tmp_path, test_context):
+    (tmp_path / "Dockerfile").write_text(dockerfile_content2)
+    docker.buildx.build(tmp_path, build_contexts=dict(test_context=test_context))
+
+
+# Test with oci layout compliant directory
+@pytest.mark.usefixtures("with_oci_layout_compliant_dir")
+@pytest.mark.usefixtures("with_container_driver")
+def test_buildx_build_build_context_oci(tmp_path):
+    (tmp_path / "Dockerfile").write_text(dockerfile_content4)
+    docker.buildx.build(
+        tmp_path,
+        build_contexts=dict(
+            test_context=f"oci-layout://{os.path.join(tmp_path, 'oci-layout')}"
+        ),
+    )
+
+
+# Test with docker image
+@pytest.mark.usefixtures("with_container_driver")
+def test_buildx_build_build_context_image(tmp_path):
+    (tmp_path / "Dockerfile").write_text(dockerfile_content3)
+    docker.buildx.build(
+        tmp_path,
+        build_contexts=dict(test_context="docker-image://busybox:1.36.0"),
+    )
+
+
+# Does the build fail when NOT passing extra contexts
+# when the dockerfile does make use of them
+@pytest.mark.usefixtures("with_container_driver")
+def test_buildx_build_build_context_fail(tmp_path):
+    (tmp_path / "Dockerfile").write_text(dockerfile_content2)
+    with pytest.raises(DockerException):
+        docker.buildx.build(tmp_path)
+
+
 def test_buildx_build_context_manager2(tmp_path):
     (tmp_path / "Dockerfile").write_text(dockerfile_content1)
     buildx_builder = docker.buildx.create()