-module(testcontainer_dockerfile). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/testcontainer_dockerfile.gleam"). -export([new/1, with_context/2, with_build_arg/3, with_timeout/2, container/1, with_expose_port/2, with_expose_ports/2, with_env/3, with_envs/2, with_secret_env/3, with_label/3, with_wait/2, with_extra_wait/2, with_command/2, with_entrypoint/2, with_name/2, with_network/2, with_bind_mount/3, with_readonly_bind/3, with_tmpfs/2, with_privileged/1, formula/1, image_id/1]). -export_type([dockerfile_config/0, docker_image/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Build custom Docker images from a Dockerfile and use them as typed\n" " formulas with [`testcontainer`](https://hex.pm/packages/testcontainer).\n" "\n" " `testcontainer` is a container runner: it does not build images.\n" " This package fills the gap by shelling out to `docker build` and\n" " wrapping the resulting image in a `Formula`.\n" ). -opaque dockerfile_config() :: {dockerfile_config, binary(), binary(), list({binary(), binary()}), list(fun((testcontainer@container:container_spec()) -> testcontainer@container:container_spec())), integer()}. -opaque docker_image() :: {docker_image, testcontainer@container:container(), binary()}. -file("src/testcontainer_dockerfile.gleam", 52). ?DOC( " Start a new Dockerfile build configuration.\n" "\n" " `dockerfile_path` should point to a readable Dockerfile, e.g.\n" " `\"./Dockerfile\"` or `\"docker/api.Dockerfile\"`. The path is trimmed\n" " before being passed to `docker build -f`.\n" ). -spec new(binary()) -> dockerfile_config(). new(Dockerfile_path) -> {dockerfile_config, Dockerfile_path, <<"."/utf8>>, [], [], 600000}. -file("src/testcontainer_dockerfile.gleam", 64). ?DOC( " Set the build context (sent to the Docker daemon as the build root).\n" " Defaults to the current directory (`.`).\n" ). -spec with_context(dockerfile_config(), binary()) -> dockerfile_config(). with_context(Cfg, Context_path) -> {dockerfile_config, erlang:element(2, Cfg), Context_path, erlang:element(4, Cfg), erlang:element(5, Cfg), erlang:element(6, Cfg)}. -file("src/testcontainer_dockerfile.gleam", 78). ?DOC( " Add a `--build-arg` flag passed to `docker build`. Use for `ARG`\n" " directives inside the Dockerfile.\n" "\n" " Note: build args are visible in the host's process list (`ps aux`)\n" " during the build window. Do **not** pass real secrets via build args;\n" " use [`with_secret_env`](#with_secret_env) for runtime secrets, or\n" " Docker BuildKit secret mounts for build-time secrets.\n" ). -spec with_build_arg(dockerfile_config(), binary(), binary()) -> dockerfile_config(). with_build_arg(Cfg, Key, Value) -> Args = lists:append(erlang:element(4, Cfg), [{Key, Value}]), {dockerfile_config, erlang:element(2, Cfg), erlang:element(3, Cfg), Args, erlang:element(5, Cfg), erlang:element(6, Cfg)}. -file("src/testcontainer_dockerfile.gleam", 89). ?DOC( " Override the default 10-minute build timeout. Clamped to the inclusive\n" " range `[1000, max_timeout_ms]`.\n" ). -spec with_timeout(dockerfile_config(), integer()) -> dockerfile_config(). with_timeout(Cfg, Ms) -> Clamped = case {Ms < 1000, Ms > 3600000} of {true, _} -> 1000; {false, true} -> 3600000; {false, false} -> Ms end, {dockerfile_config, erlang:element(2, Cfg), erlang:element(3, Cfg), erlang:element(4, Cfg), erlang:element(5, Cfg), Clamped}. -file("src/testcontainer_dockerfile.gleam", 287). ?DOC( " Get the running `Container` so you can use `container.host_port`,\n" " `container.host`, `container.mapped_url`, etc.\n" ). -spec container(docker_image()) -> testcontainer@container:container(). container(Img) -> erlang:element(2, Img). -file("src/testcontainer_dockerfile.gleam", 464). -spec add_transform( dockerfile_config(), fun((testcontainer@container:container_spec()) -> testcontainer@container:container_spec()) ) -> dockerfile_config(). add_transform(Cfg, T) -> {dockerfile_config, erlang:element(2, Cfg), erlang:element(3, Cfg), erlang:element(4, Cfg), lists:append(erlang:element(5, Cfg), [T]), erlang:element(6, Cfg)}. -file("src/testcontainer_dockerfile.gleam", 103). ?DOC(" Map a port that should be exposed when the container runs.\n"). -spec with_expose_port(dockerfile_config(), testcontainer@port:port_()) -> dockerfile_config(). with_expose_port(Cfg, P) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:expose_port(Spec, P) end ). -file("src/testcontainer_dockerfile.gleam", 111). ?DOC(" Map several exposed ports at once.\n"). -spec with_expose_ports(dockerfile_config(), list(testcontainer@port:port_())) -> dockerfile_config(). with_expose_ports(Cfg, Ports) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:expose_ports(Spec, Ports) end ). -file("src/testcontainer_dockerfile.gleam", 120). ?DOC( " Add an environment variable injected at container start. Overrides\n" " any `ENV` baked into the image.\n" ). -spec with_env(dockerfile_config(), binary(), binary()) -> dockerfile_config(). with_env(Cfg, Key, Value) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:with_env(Spec, Key, Value) end ). -file("src/testcontainer_dockerfile.gleam", 129). ?DOC(" Add many env vars at once.\n"). -spec with_envs(dockerfile_config(), list({binary(), binary()})) -> dockerfile_config(). with_envs(Cfg, Pairs) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:with_envs(Spec, Pairs) end ). -file("src/testcontainer_dockerfile.gleam", 138). ?DOC( " Add an env var whose value is wrapped in `cowl.Secret` and never\n" " appears in `string.inspect` output.\n" ). -spec with_secret_env(dockerfile_config(), binary(), cowl:secret(binary())) -> dockerfile_config(). with_secret_env(Cfg, Key, Secret) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:with_secret_env(Spec, Key, Secret) end ). -file("src/testcontainer_dockerfile.gleam", 147). ?DOC(" Add a Docker label to the running container.\n"). -spec with_label(dockerfile_config(), binary(), binary()) -> dockerfile_config(). with_label(Cfg, Key, Value) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:with_label(Spec, Key, Value) end ). -file("src/testcontainer_dockerfile.gleam", 157). ?DOC( " Set the wait strategy used after the container starts. Replaces any\n" " previous wait strategy.\n" ). -spec with_wait(dockerfile_config(), testcontainer@wait:wait_strategy()) -> dockerfile_config(). with_wait(Cfg, Strategy) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:wait_for(Spec, Strategy) end ). -file("src/testcontainer_dockerfile.gleam", 167). ?DOC( " Combine an additional wait strategy with whatever is already configured\n" " using `wait.all_of`. Useful to layer e.g. `wait.health_check()` on top\n" " of a base port wait.\n" ). -spec with_extra_wait(dockerfile_config(), testcontainer@wait:wait_strategy()) -> dockerfile_config(). with_extra_wait(Cfg, Extra) -> add_transform( Cfg, fun(Spec) -> Current = testcontainer@container:wait_strategy(Spec), testcontainer@container:wait_for( Spec, testcontainer@wait:all_of([Current, Extra]) ) end ). -file("src/testcontainer_dockerfile.gleam", 178). ?DOC(" Override the image's CMD when running the container.\n"). -spec with_command(dockerfile_config(), list(binary())) -> dockerfile_config(). with_command(Cfg, Cmd) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:with_command(Spec, Cmd) end ). -file("src/testcontainer_dockerfile.gleam", 186). ?DOC(" Override the image's ENTRYPOINT.\n"). -spec with_entrypoint(dockerfile_config(), list(binary())) -> dockerfile_config(). with_entrypoint(Cfg, Ep) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:with_entrypoint(Spec, Ep) end ). -file("src/testcontainer_dockerfile.gleam", 194). ?DOC(" Set a fixed container name.\n"). -spec with_name(dockerfile_config(), binary()) -> dockerfile_config(). with_name(Cfg, Name) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:with_name(Spec, Name) end ). -file("src/testcontainer_dockerfile.gleam", 199). ?DOC(" Attach the container to a Docker network by name.\n"). -spec with_network(dockerfile_config(), binary()) -> dockerfile_config(). with_network(Cfg, Network) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:on_network(Spec, Network) end ). -file("src/testcontainer_dockerfile.gleam", 207). ?DOC(" Bind-mount a host path read-write into the container.\n"). -spec with_bind_mount(dockerfile_config(), binary(), binary()) -> dockerfile_config(). with_bind_mount(Cfg, Host_path, Container_path) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:with_bind_mount( Spec, Host_path, Container_path ) end ). -file("src/testcontainer_dockerfile.gleam", 218). ?DOC(" Bind-mount a host path read-only into the container.\n"). -spec with_readonly_bind(dockerfile_config(), binary(), binary()) -> dockerfile_config(). with_readonly_bind(Cfg, Host_path, Container_path) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:with_readonly_bind( Spec, Host_path, Container_path ) end ). -file("src/testcontainer_dockerfile.gleam", 229). ?DOC(" Mount an in-memory tmpfs at the given container path.\n"). -spec with_tmpfs(dockerfile_config(), binary()) -> dockerfile_config(). with_tmpfs(Cfg, Container_path) -> add_transform( Cfg, fun(Spec) -> testcontainer@container:with_tmpfs(Spec, Container_path) end ). -file("src/testcontainer_dockerfile.gleam", 240). ?DOC( " Run the container as privileged. **Use with extreme caution**: a\n" " privileged container has full access to host devices and can escape\n" " most isolation. Naming alone (`with_privileged`) is the security\n" " signal — there is no runtime warning.\n" ). -spec with_privileged(dockerfile_config()) -> dockerfile_config(). with_privileged(Cfg) -> add_transform(Cfg, fun testcontainer@container:with_privileged/1). -file("src/testcontainer_dockerfile.gleam", 474). -spec apply_transforms( testcontainer@container:container_spec(), list(fun((testcontainer@container:container_spec()) -> testcontainer@container:container_spec())) ) -> testcontainer@container:container_spec(). apply_transforms(Spec, Transforms) -> gleam@list:fold(Transforms, Spec, fun(S, T) -> T(S) end). -file("src/testcontainer_dockerfile.gleam", 489). -spec build_dockerfile(dockerfile_config()) -> {ok, binary()} | {error, testcontainer_dockerfile@error:error()}. build_dockerfile(Cfg) -> case testcontainer_dockerfile_ffi:run_docker_build( erlang:element(2, Cfg), erlang:element(3, Cfg), erlang:element(4, Cfg), erlang:element(6, Cfg) ) of {ok, Image_id} -> {ok, Image_id}; {error, <<"DOCKER_NOT_FOUND"/utf8>>} -> {error, docker_not_found}; {error, Reason} -> {error, {build_failed, erlang:element(2, Cfg), Reason}} end. -file("src/testcontainer_dockerfile.gleam", 373). -spec has_unsafe_char(binary()) -> boolean(). has_unsafe_char(S) -> (gleam_stdlib:contains_string(S, <<"\x{0000}"/utf8>>) orelse gleam_stdlib:contains_string( S, <<"\n"/utf8>> )) orelse gleam_stdlib:contains_string(S, <<"\r"/utf8>>). -file("src/testcontainer_dockerfile.gleam", 453). -spec is_digit(binary()) -> boolean(). is_digit(C) -> case C of <<"0"/utf8>> -> true; <<"1"/utf8>> -> true; <<"2"/utf8>> -> true; <<"3"/utf8>> -> true; <<"4"/utf8>> -> true; <<"5"/utf8>> -> true; <<"6"/utf8>> -> true; <<"7"/utf8>> -> true; <<"8"/utf8>> -> true; <<"9"/utf8>> -> true; _ -> false end. -file("src/testcontainer_dockerfile.gleam", 395). -spec is_alpha(binary()) -> boolean(). is_alpha(C) -> case C of <<"a"/utf8>> -> true; <<"b"/utf8>> -> true; <<"c"/utf8>> -> true; <<"d"/utf8>> -> true; <<"e"/utf8>> -> true; <<"f"/utf8>> -> true; <<"g"/utf8>> -> true; <<"h"/utf8>> -> true; <<"i"/utf8>> -> true; <<"j"/utf8>> -> true; <<"k"/utf8>> -> true; <<"l"/utf8>> -> true; <<"m"/utf8>> -> true; <<"n"/utf8>> -> true; <<"o"/utf8>> -> true; <<"p"/utf8>> -> true; <<"q"/utf8>> -> true; <<"r"/utf8>> -> true; <<"s"/utf8>> -> true; <<"t"/utf8>> -> true; <<"u"/utf8>> -> true; <<"v"/utf8>> -> true; <<"w"/utf8>> -> true; <<"x"/utf8>> -> true; <<"y"/utf8>> -> true; <<"z"/utf8>> -> true; <<"A"/utf8>> -> true; <<"B"/utf8>> -> true; <<"C"/utf8>> -> true; <<"D"/utf8>> -> true; <<"E"/utf8>> -> true; <<"F"/utf8>> -> true; <<"G"/utf8>> -> true; <<"H"/utf8>> -> true; <<"I"/utf8>> -> true; <<"J"/utf8>> -> true; <<"K"/utf8>> -> true; <<"L"/utf8>> -> true; <<"M"/utf8>> -> true; <<"N"/utf8>> -> true; <<"O"/utf8>> -> true; <<"P"/utf8>> -> true; <<"Q"/utf8>> -> true; <<"R"/utf8>> -> true; <<"S"/utf8>> -> true; <<"T"/utf8>> -> true; <<"U"/utf8>> -> true; <<"V"/utf8>> -> true; <<"W"/utf8>> -> true; <<"X"/utf8>> -> true; <<"Y"/utf8>> -> true; <<"Z"/utf8>> -> true; _ -> false end. -file("src/testcontainer_dockerfile.gleam", 391). -spec is_valid_arg_char(binary()) -> boolean(). is_valid_arg_char(C) -> (is_alpha(C) orelse is_digit(C)) orelse (C =:= <<"_"/utf8>>). -file("src/testcontainer_dockerfile.gleam", 387). -spec is_valid_first_char(binary()) -> boolean(). is_valid_first_char(C) -> is_alpha(C) orelse (C =:= <<"_"/utf8>>). -file("src/testcontainer_dockerfile.gleam", 379). -spec is_valid_build_arg_key(binary()) -> boolean(). is_valid_build_arg_key(Key) -> case gleam@string:to_graphemes(Key) of [] -> false; [First | Rest] -> is_valid_first_char(First) andalso gleam@list:all( Rest, fun is_valid_arg_char/1 ) end. -file("src/testcontainer_dockerfile.gleam", 344). -spec validate_build_args(list({binary(), binary()})) -> {ok, nil} | {error, testcontainer_dockerfile@error:error()}. validate_build_args(Args) -> case Args of [] -> {ok, nil}; [{Key, Value} | Rest] -> case is_valid_build_arg_key(Key) of false -> {error, {build_failed, <<""/utf8>>, <<<<"invalid build-arg key: "/utf8, Key/binary>>/binary, " (must match [A-Za-z_][A-Za-z0-9_]*)"/utf8>>}}; true -> case has_unsafe_char(Value) of true -> {error, {build_failed, <<""/utf8>>, <<<<"build-arg value for "/utf8, Key/binary>>/binary, " contains NUL or CR/LF characters"/utf8>>}}; false -> validate_build_args(Rest) end end end. -file("src/testcontainer_dockerfile.gleam", 336). -spec validate_dockerfile_is_file(binary()) -> {ok, nil} | {error, testcontainer_dockerfile@error:error()}. validate_dockerfile_is_file(Path) -> case fio:is_file(Path) of {ok, true} -> {ok, nil}; {ok, false} -> {error, {dockerfile_not_found, Path}}; {error, _} -> {error, {dockerfile_not_found, Path}} end. -file("src/testcontainer_dockerfile.gleam", 313). -spec validate_path_string(binary(), binary()) -> {ok, nil} | {error, testcontainer_dockerfile@error:error()}. validate_path_string(Path, Kind) -> case Path of <<""/utf8>> -> case Kind of <<"dockerfile"/utf8>> -> {error, {dockerfile_not_found, <<""/utf8>>}}; _ -> {error, {build_failed, Path, <<"context path is empty"/utf8>>}} end; _ -> case has_unsafe_char(Path) of true -> {error, {build_failed, Path, <>}}; false -> {ok, nil} end end. -file("src/testcontainer_dockerfile.gleam", 295). -spec validate_and_normalize(dockerfile_config()) -> {ok, dockerfile_config()} | {error, testcontainer_dockerfile@error:error()}. validate_and_normalize(Cfg) -> Trimmed_dockerfile = gleam@string:trim(erlang:element(2, Cfg)), Trimmed_context = gleam@string:trim(erlang:element(3, Cfg)), gleam@result:'try'( validate_path_string(Trimmed_dockerfile, <<"dockerfile"/utf8>>), fun(_) -> gleam@result:'try'( validate_path_string(Trimmed_context, <<"context"/utf8>>), fun(_) -> gleam@result:'try'( validate_dockerfile_is_file(Trimmed_dockerfile), fun(_) -> gleam@result:'try'( validate_build_args(erlang:element(4, Cfg)), fun(_) -> {ok, {dockerfile_config, Trimmed_dockerfile, Trimmed_context, erlang:element(4, Cfg), erlang:element(5, Cfg), erlang:element(6, Cfg)}} end ) end ) end ) end ). -file("src/testcontainer_dockerfile.gleam", 262). ?DOC( " Run `docker build` and return a `Formula` that wraps the resulting\n" " image. The build happens eagerly when this function is called.\n" "\n" " On success, the returned `Formula` can be passed straight to\n" " `testcontainer.with_formula` (see the README for an end-to-end example).\n" "\n" " Failure modes (`error.Error` variants):\n" " - `DockerNotFound` — the `docker` CLI is not on PATH\n" " - `DockerfileNotFound(path)` — the configured Dockerfile path is\n" " empty, contains invalid characters, points to a directory, or does\n" " not exist on disk\n" " - `BuildFailed(path, reason)` — `docker build` returned non-zero\n" " or invalid build-arg keys were rejected. `reason` carries full\n" " stdout+stderr for diagnostics\n" ). -spec formula(dockerfile_config()) -> {ok, testcontainer@formula:formula(docker_image())} | {error, testcontainer_dockerfile@error:error()}. formula(Cfg) -> gleam@result:'try'( validate_and_normalize(Cfg), fun(Trimmed_cfg) -> gleam@result:'try'( build_dockerfile(Trimmed_cfg), fun(Image_id) -> Spec = apply_transforms( testcontainer@container:new(Image_id), erlang:element(5, Trimmed_cfg) ), {ok, testcontainer@formula:new( Spec, fun(Running) -> {ok, {docker_image, Running, Image_id}} end )} end ) end ). -file("src/testcontainer_dockerfile.gleam", 281). ?DOC(" Get the Docker image ID (e.g. `sha256:abcd...`) of a built image.\n"). -spec image_id(docker_image()) -> binary(). image_id(Img) -> erlang:element(3, Img).