Skip to content

Commit

Permalink
First pass at publishing the scie-plugin (#92)
Browse files Browse the repository at this point in the history
- Updated Readme with more usage examples
- Fixed unstripped path problem
- Formatted everything
  • Loading branch information
sureshjoshi authored May 23, 2023
1 parent 6d2c3f2 commit 31e238c
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 81 deletions.
6 changes: 1 addition & 5 deletions examples/python/hellofastapi/BUILD.pants
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
python_sources(
name="libhellofastapi",
sources=["**/*.py"],
dependencies=[
"//:reqs#uvicorn",
],
)

pex_binary(
name="hellofastapi-pex",
entry_point="hellofastapi.main",
dependencies=[":libhellofastapi"],
dependencies=[":libhellofastapi", "//:reqs#uvicorn"],
include_tools=True,
)

Expand Down
11 changes: 0 additions & 11 deletions examples/python/hellofastapi/hellofastapi/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import uvicorn
from fastapi import FastAPI

app = FastAPI()
Expand All @@ -7,13 +6,3 @@
@app.get("/")
async def root() -> dict[str, str]:
return {"message": "Hello World"}


def main() -> None:
# Don't actually run FastAPI like this - it's just a PyOx proof-of-concept
uvicorn.run(app, host="localhost", port=8000)


if __name__ == "__main__":
print("Launching HelloFastAPI from __main__")
main()
21 changes: 21 additions & 0 deletions pants-plugins/experimental/scie/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,24 @@
# Licensed under the Apache License, Version 2.0 (see LICENSE).

python_sources()

python_distribution(
name="scie-dist",
dependencies=[":scie"],
repositories=["@pypi"], # Configuration in ~/.pypirc
wheel=True,
sdist=False,
provides=python_artifact(
name="robotpajamas.pants.scie",
version="0.0.2",
description="A Pants plugin adding support for packaging SCIE.",
license="Apache License, Version 2.0",
project_urls={
"Documentation": "https://github.com/sureshjoshi/pants-plugins",
"Source": "https://github.com/sureshjoshi/pants-plugins",
"Tracker": "https://github.com/sureshjoshi/pants-plugins/issues",
},
long_description_content_type="text/markdown",
long_description=open("pants-plugins/experimental/scie/README.md").read(),
),
)
136 changes: 135 additions & 1 deletion pants-plugins/experimental/scie/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,135 @@
# pants-scie-plugin
# pants-scie-plugin

This plugin provides a `scie_binary` target that can be used to create a single-file Python executable with an embedded Python interpreter, built via [scie-jump](https://github.com/a-scie/jump).

It uses [science](https://github.com/a-scie/lift) and a `.toml` configuration file to build the executable, expecting a `pex_binary` as a dependency.

## Installation

This plugin was developed using Python 3.9 and Pants 2.16.0.rc3, but should work with Pants 2.15+ (however, your mileage may vary).

Add the following to your `pants.toml` file:

```toml
[GLOBAL]
plugins = [
...
"robotpajamas.pants.scie",
]

...

backend_packages = [
...
"experimental.scie",
]
```

## Usage

At the moment, the `scie_binary` expects a `pex_binary` as its only dependency. There is no technical limitation to this, but it simplified the initial implementation.

For trivial packaging, you can set an `entry_point` on your `pex_binary` and the `scie_binary` will directly call your pex (e.g. `python myapp.pex`). This can be particularly useful for CLIs or other simple applications. The name of your binary will be the name of your `scie_binary` target, and it will be placed in the `dist` directory.

```python
# BUILD

pex_binary(
name="mycli-pex",
entry_point="mycli.main",
...
)

scie_binary(
name="mycli",
dependencies=[":mycli-pex"],
...
)
```

You can optionally cross-build your executable by setting the `platforms` field on your `scie_binary` target. This will create a binary for the specified platform, and will be named accordingly (e.g. `mycli-linux-x86_64`). You should also ensure that your `pex_binary` is built for the same platform(s).

```python
# BUILD

pex_binary(
name="mycli-pex",
entry_point="mycli.main",
platforms=["linux-x86_64-cp-311-cp311", "macosx-13.3-arm64-cp-311-cp311",]
...
)

scie_binary(
name="mycli",
dependencies=[":mycli-pex"],
platforms=["linux-x86_64", "macos-aarch64",]
...
)
```

## Advanced Usage

For non-trivial packaging, it is much easier (and cleaner) to use the `science` config TOML file to specify what should be in the package and how it should work. This may be in situations where you want multiple commands, or you require boot bindings. A good example of this is setting up a FastAPI application with a Uvicorn or Gunicorn runner, which requires using PEX_TOOLS and creating a `venv` from your code.

The plugin will attempt to replace variables in the TOML with equivalent ones specified in the target. For example, the binary name, the binary description, `platforms` (if specified), etc... The one critical aspect to note is that in order to reference another target (e.g. the output of `pex_binary`), use the `:target_name` syntax in the TOML. The plugin will use `science`'s `--file` mapping argument to replace the target name with the actual file path.

```python
# BUILD

pex_binary(
name="hellofastapi-pex",
entry_point="hellofastapi.main",
dependencies=[":libhellofastapi"],
include_tools=True,
)

scie_binary(
name="hellofastapi",
dependencies=[":hellofastapi-pex"],
lift="lift.toml",
...
)
```

```toml
# lift.toml

[lift]
name = "hellofastapi"
description = "An example FastAPI Lift application including using an external uvicorn server"

[[lift.interpreters]]
id = "cpython"
provider = "PythonBuildStandalone"
release = "20230507"
version = "3.11.3"

[[lift.files]]
# Note the leading colon, which is required to reference the pex_binary dependency
name = ":hellofastapi-pex"

[[lift.commands]]
exe = "{scie.bindings.venv}/venv/bin/uvicorn"
args = ["hellofastapi.main:app", "--port", "7999"]
description = "The FastAPI executable."

[[lift.bindings]]
name = "venv"
description = "Installs HelloFastAPI into a venv and pre-compiles .pyc"
exe = "#{cpython:python}"
args = [
"{:hellofastapi-pex}",
"venv",
"--bin-path",
"prepend",
"--compile",
"--rm",
"all",
"{scie.bindings}/venv",
]

[lift.bindings.env.default]
"=PATH" = "{cpython}/python/bin:{scie.env.PATH}"
"PEX_TOOLS" = "1"
"PEX_ROOT" = "{scie.bindings}/pex_root"
```
23 changes: 15 additions & 8 deletions pants-plugins/experimental/scie/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,70 @@

from __future__ import annotations

from dataclasses import dataclass
import logging
from dataclasses import dataclass

import toml

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class Config:
lift: LiftConfig

def __post_init__(self):
if isinstance(self.lift, dict):
logger.warning(f"LIFT: {self.lift}")
object.__setattr__(self, 'lift', LiftConfig(**self.lift))
object.__setattr__(self, "lift", LiftConfig(**self.lift))

@classmethod
def from_toml(cls, file_path):
with open(file_path) as f:
data = toml.load(f)
return cls(**data)


@dataclass(frozen=True)
class LiftConfig:
"""
This configuration is a subset of the configuration that can be found here:
https://github.com/a-scie/lift/blob/main/science/model.py
"""

name: str
description: str
platforms: list[str]
interpreters: list[Interpreter]
interpreters: list[Interpreter]
files: list[File]
commands: list[Command]
bindings: frozenset[Command] = frozenset()

def __post_init__(self):
if any(isinstance(i, dict) for i in self.interpreters):
object.__setattr__(self, 'interpreters', [Interpreter(**i) for i in self.interpreters]) # type: ignore
object.__setattr__(self, "interpreters", [Interpreter(**i) for i in self.interpreters]) # type: ignore
if any(isinstance(f, dict) for f in self.files):
object.__setattr__(self, 'files', [File(**f) for f in self.files]) # type: ignore
object.__setattr__(self, "files", [File(**f) for f in self.files]) # type: ignore
if any(isinstance(c, dict) for c in self.commands):
object.__setattr__(self, 'commands', [Command(**c) for c in self.commands]) # type: ignore
object.__setattr__(self, "commands", [Command(**c) for c in self.commands]) # type: ignore
if any(isinstance(b, dict) for b in self.bindings):
object.__setattr__(self, 'bindings', [Command(**b) for b in self.bindings]) # type: ignore
object.__setattr__(self, "bindings", [Command(**b) for b in self.bindings]) # type: ignore


@dataclass(frozen=True)
class Interpreter:
version: str
id: str = "cpython"
provider: str = "PythonBuildStandalone"
release: str = "3.11.3"
release: str = "20230507"
lazy: bool = True


@dataclass(frozen=True)
class File:
name: str


@dataclass(frozen=True)
class Command:
exe: str
Expand Down
Loading

0 comments on commit 31e238c

Please sign in to comment.