@@ -350,6 +350,75 @@ def update_hash(self, hasher: hashlib._Hash, ignore: Optional[Any] = None):
350350 ).update_hash (hasher , ignore = ignore )
351351
352352
353+ @rich .repr .auto
354+ @dataclass (frozen = True , repr = True )
355+ class PixiScript (Layer ):
356+ """
357+ A standalone Python script whose dependencies are declared in a PEP 723 block with
358+ pixi-specific `[tool.pixi.*]` tables.
359+
360+ Pixi has no `install --script`, so at build time the script's metadata is lowered into
361+ an equivalent `pixi.toml` workspace manifest and installed like any other
362+ [`PixiProject`][flyte.Image.with_pixi_project]. Like `PixiProject`, pixi resolves conda
363+ and PyPI packages from its own manifest, so this layer does not inherit `PipOption`.
364+ """
365+
366+ script : Path
367+ script_name : str = field (init = False )
368+ #: Conda subdirs the generated workspace supports, derived from the image's platforms.
369+ #: A `platforms` key declared by the script itself takes precedence over these.
370+ platforms : Tuple [str , ...] = ("linux-64" ,)
371+ environment : str = "default"
372+ extra_args : Optional [str ] = None
373+ secret_mounts : Optional [Tuple [str | Secret , ...]] = None
374+
375+ def __post_init__ (self ):
376+ object .__setattr__ (self , "script_name" , self .script .name )
377+ super ().__post_init__ ()
378+
379+ @property
380+ def pixi_lock (self ) -> Optional [Path ]:
381+ """The script's sidecar lock file (`<script>.pixi.lock`), if `pixi lock --script` made one."""
382+ lock = self .script .with_name (f"{ self .script .name } .pixi.lock" )
383+ return lock if lock .exists () else None
384+
385+ def validate (self ):
386+ if not self .script .exists ():
387+ raise FileNotFoundError (f"Pixi script { self .script } does not exist" )
388+ if not self .script .is_file ():
389+ raise ValueError (f"Pixi script { self .script } is not a file" )
390+ if self .script .suffix != ".py" :
391+ raise ValueError (f"Pixi script { self .script } must have a .py extension" )
392+
393+ from ._utils import check_pixi_platforms_supported , parse_pixi_script_file
394+
395+ check_pixi_platforms_supported (parse_pixi_script_file (self .script ), self .platforms )
396+ super ().validate ()
397+
398+ def render_manifest (self ) -> str :
399+ """The source of the `pixi.toml` this script lowers to."""
400+ from ._utils import parse_pixi_script_file , render_pixi_manifest
401+
402+ return render_pixi_manifest (parse_pixi_script_file (self .script ), self .platforms )
403+
404+ def update_hash (self , hasher : hashlib ._Hash , ignore : Optional [Any ] = None ):
405+ # Hash the generated manifest rather than the script body: only the PEP 723 block
406+ # affects the image, so edits to the script's code reuse the built image.
407+ hash_input = self .render_manifest () + self .environment
408+ if self .extra_args :
409+ hash_input += self .extra_args
410+ if self .secret_mounts :
411+ for secret_mount in self .secret_mounts :
412+ hash_input += str (secret_mount )
413+ hasher .update (hash_input .encode ("utf-8" ))
414+
415+ pixi_lock = self .pixi_lock
416+ if pixi_lock is not None :
417+ from ._utils import filehash_update
418+
419+ filehash_update (pixi_lock , hasher )
420+
421+
353422@rich .repr .auto
354423@dataclass (frozen = True , repr = True )
355424class AptPackages (Layer ):
@@ -515,6 +584,9 @@ def from_dict(cls, envs: Dict[str, str]) -> Env:
515584
516585Architecture = Literal ["linux/amd64" , "linux/arm64" ]
517586
587+ # Platforms a default image is built for when the caller does not name any.
588+ _DEFAULT_PLATFORMS : Tuple [Architecture , ...] = ("linux/amd64" , "linux/arm64" )
589+
518590_BASE_REGISTRY = "ghcr.io/flyteorg"
519591_LOCALHOST_REGISTRY = "localhost:30000"
520592_DEFAULT_IMAGE_NAME = "flyte"
@@ -610,6 +682,7 @@ class Image:
610682 - `from_debian_base()` — Debian-based image with a specified Python version
611683 - `from_base()` — Any base image by name (e.g., `"python:3.12-slim"`)
612684 - `from_uv_script()` — Image from a `uv`-compatible script with inline dependencies
685+ - `from_pixi_script()` — Image from a `pixi`-compatible script with inline dependencies
613686 - `from_dockerfile()` — Image from a custom Dockerfile
614687 - `from_ref_name()` — Reference to a pre-built image by name
615688
@@ -719,15 +792,15 @@ def _get_default_image_for(
719792 registry = _get_push_registry (),
720793 name = _DEFAULT_IMAGE_NAME ,
721794 python_version = python_version ,
722- platform = ( "linux/amd64" , "linux/arm64" ) if platform is None else platform ,
795+ platform = _DEFAULT_PLATFORMS if platform is None else platform ,
723796 extendable = True ,
724797 )
725798 image = Image ._new (
726799 base_image = f"python:{ python_version [0 ]} .{ python_version [1 ]} -slim-bookworm" ,
727800 registry = _get_push_registry (),
728801 name = _DEFAULT_IMAGE_NAME ,
729802 python_version = python_version ,
730- platform = ( "linux/amd64" , "linux/arm64" ) if platform is None else platform ,
803+ platform = _DEFAULT_PLATFORMS if platform is None else platform ,
731804 extendable = True ,
732805 )
733806 labels = _DockerLines (
@@ -930,6 +1003,99 @@ def from_uv_script(
9301003
9311004 return img .clone (addl_layer = ll )
9321005
1006+ @classmethod
1007+ def from_pixi_script (
1008+ cls ,
1009+ script : Path | str ,
1010+ * ,
1011+ name : str ,
1012+ registry : str | None = None ,
1013+ registry_secret : Optional [str | Secret ] = None ,
1014+ environment : str = "default" ,
1015+ extra_args : Optional [str ] = None ,
1016+ platform : Optional [Tuple [Architecture , ...]] = None ,
1017+ secret_mounts : Optional [SecretRequest ] = None ,
1018+ ) -> Image :
1019+ """
1020+ Create an image from a `pixi`-compatible script, using the PEP 723 block at the top of
1021+ the script to determine the Python version and the conda and PyPI packages to install.
1022+
1023+ A pixi script declares portable PEP 723 fields alongside pixi-specific `[tool.pixi.*]`
1024+ tables, which is what lets it pull conda packages that `from_uv_script` cannot:
1025+
1026+ ```python
1027+ #!/usr/bin/env -S pixi run --script
1028+ # /// script
1029+ # requires-python = ">=3.12"
1030+ # dependencies = ["flyte"]
1031+ #
1032+ # [tool.pixi.workspace]
1033+ # channels = ["conda-forge"]
1034+ #
1035+ # [tool.pixi.dependencies]
1036+ # gdal = "*"
1037+ # ///
1038+ ```
1039+
1040+ `requires-python` becomes the environment's Python (an explicit
1041+ `[tool.pixi.dependencies].python` wins over it), `dependencies` become PyPI packages, and
1042+ `[tool.pixi.dependencies]` become conda packages. Channels default to `conda-forge` and
1043+ the supported platforms default to those of the image, unless `[tool.pixi.workspace]`
1044+ declares its own. Every other `[tool.pixi.*]` table (`target`, `feature`, `activation`,
1045+ ...) is passed through to pixi untouched.
1046+
1047+ If a sidecar lock file created by `pixi lock --script <script>` sits next to the script
1048+ as `<script>.pixi.lock`, it is used and the install is run with `--locked`. Note that
1049+ pixi locks only the platforms the workspace declares, so to lock an image built for
1050+ `linux/amd64` the script should declare `platforms = ["linux-64"]` under
1051+ `[tool.pixi.workspace]` before locking.
1052+
1053+ Unlike `from_uv_script`, flyte is not installed for you: list `flyte` in the script's
1054+ `dependencies` so the task can run in the resulting environment.
1055+
1056+ For more information on the pixi script format, see the documentation:
1057+ [Pixi: Python scripts](https://pixi.prefix.dev/latest/python/scripts/)
1058+
1059+ Args:
1060+ script: path to the pixi script
1061+ name: name of the image
1062+ registry: registry to use for the image
1063+ registry_secret: Secret to use to pull/push the private image.
1064+ environment: pixi environment to install, default is "default"
1065+ extra_args: extra arguments to pass to `pixi install`, default is None
1066+ platform: architecture to use for the image, default is linux/amd64, use tuple for
1067+ multiple values
1068+ secret_mounts: Secret mounts to use for the image, default is None.
1069+
1070+ Returns:
1071+ Image
1072+ """
1073+ from ._utils import pixi_platforms_for
1074+
1075+ # The generated manifest must name the platforms it supports, so resolve them from the
1076+ # image's platforms up front and keep them on the layer: the layer has no way to reach
1077+ # back to the image at build time. Mirrors the multi-arch default `from_debian_base`
1078+ # applies when no platform is given.
1079+ resolved_platform = platform or _DEFAULT_PLATFORMS
1080+
1081+ ll = PixiScript (
1082+ script = Path (script ),
1083+ platforms = pixi_platforms_for (resolved_platform ),
1084+ environment = environment ,
1085+ extra_args = extra_args ,
1086+ secret_mounts = _ensure_tuple (secret_mounts ) if secret_mounts else None ,
1087+ )
1088+
1089+ img = cls .from_debian_base (
1090+ registry = registry ,
1091+ registry_secret = registry_secret ,
1092+ install_flyte = False ,
1093+ name = name ,
1094+ platform = platform ,
1095+ )
1096+
1097+ return img .clone (addl_layer = ll )
1098+
9331099 def clone (
9341100 self ,
9351101 registry : Optional [str ] = None ,
0 commit comments