Skip to content

Commit 7e02540

Browse files
committed
Fix remote build context identity and preserve hard-linked files
1 parent 96ec380 commit 7e02540

9 files changed

Lines changed: 686 additions & 43 deletions

File tree

‎README.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,42 @@ for event in connection.events():
352352
print(event)
353353
```
354354

355+
### Cache remote Dockerfile builds
356+
357+
Use the public context fingerprint when deriving a cache name. It uses the same
358+
Dockerfile source selection and `.dockerignore` rules as remote packaging,
359+
including file contents, modes, paths, and symlinks. It ignores timestamps and
360+
does not compress or stage the context on disk.
361+
362+
```python
363+
from hyperbrowser.build_context import docker_build_context_fingerprint
364+
365+
fingerprint = docker_build_context_fingerprint("./app")
366+
# Include build options such as platform and image_init in your cache key too.
367+
image_name = f"app-{fingerprint[:32]}"
368+
build = client.sandboxes.build_image_from_dockerfile(
369+
context_path="./app",
370+
image_name=image_name,
371+
expected_context_fingerprint=fingerprint,
372+
)
373+
```
374+
375+
If the archived inputs differ from the fingerprint, the SDK raises
376+
`DockerBuildContextChangedError` before creating or uploading a build. Compute a
377+
fresh fingerprint and repeat the lookup/build operation. Use the same `dockerfile`
378+
and `force_full_context` selection when fingerprinting and building (the latter
379+
is named `remote_full_context` on the build method). The expected fingerprint is
380+
supported only for remote builds.
381+
382+
Fingerprinting streams included files and performs blocking I/O. Async callers
383+
should use `await asyncio.to_thread(docker_build_context_fingerprint, "./app")`
384+
(Python 3.9+) or an executor. It does not resolve mutable base-image tags or
385+
network resources fetched by a Dockerfile; rebuild explicitly when those change.
386+
387+
Image listings distinguish `ready` from `uploaded`: a completed team image can
388+
be ready to launch before its durability backup is uploaded. `ready` is `None`
389+
when talking to an older server. Keep the returned image ID to pin that revision.
390+
355391
## License
356392

357393
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

‎hyperbrowser/build_context.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Public helpers for identifying inputs to remote Dockerfile builds."""
2+
3+
from .client.managers.sandboxes.image_build import (
4+
DockerBuildContextChangedError,
5+
docker_build_context_fingerprint,
6+
)
7+
8+
__all__ = ["DockerBuildContextChangedError", "docker_build_context_fingerprint"]

‎hyperbrowser/client/managers/async_manager/sandbox.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,7 @@ async def _build_image_from_remote_dockerfile(
725725
dockerfile,
726726
platform: str,
727727
remote_full_context: bool,
728+
expected_context_fingerprint: Optional[str],
728729
image_init: Optional[Union[SandboxImageInitDict, SandboxImageInit]],
729730
image_config_user: Optional[str],
730731
builder_cpus: Optional[int],
@@ -741,6 +742,7 @@ async def _build_image_from_remote_dockerfile(
741742
context_path,
742743
dockerfile=dockerfile,
743744
force_full_context=remote_full_context,
745+
expected_context_fingerprint=expected_context_fingerprint,
744746
temp_dir=temp_dir,
745747
)
746748
build_id = None
@@ -803,6 +805,7 @@ async def build_image_from_dockerfile(
803805
dockerfile="Dockerfile",
804806
remote: bool = True,
805807
remote_full_context: bool = False,
808+
expected_context_fingerprint: Optional[str] = None,
806809
docker_tag: Optional[str] = None,
807810
platform: str = IMAGE_BUILD_SOURCE_PLATFORM,
808811
build_args: Optional[Dict[str, str]] = None,
@@ -829,6 +832,7 @@ async def build_image_from_dockerfile(
829832
dockerfile=dockerfile,
830833
platform=platform,
831834
remote_full_context=remote_full_context,
835+
expected_context_fingerprint=expected_context_fingerprint,
832836
image_init=image_init,
833837
image_config_user=image_config_user,
834838
builder_cpus=builder_cpus,
@@ -840,6 +844,8 @@ async def build_image_from_dockerfile(
840844
temp_dir=temp_dir,
841845
upload_timeout=upload_timeout,
842846
)
847+
if expected_context_fingerprint is not None:
848+
raise ValueError("expected_context_fingerprint requires remote=True")
843849
tag = docker_tag or make_temp_docker_tag()
844850
remove_tag = docker_tag is None
845851
try:

0 commit comments

Comments
 (0)