Skip to content

Commit

Permalink
Add support for console scripts (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
tusharsadhwani authored May 19, 2024
1 parent 0fae23d commit 23cc255
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 4 deletions.
16 changes: 13 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
XDG_CACHE_HOME: ${{ github.workspace }}/.cache

jobs:
build:
name: ${{ matrix.name }}
Expand Down Expand Up @@ -71,6 +68,11 @@ jobs:
TEXTUAL_PATH=${PWD}/${TEXTUAL_NAME}
packaged $TEXTUAL_NAME 'pip install pygame' 'python -m textual'
# IPython: requires no source
IPYTHON_NAME="ipython-${{ matrix.target }}.bin"
IPYTHON_PATH=${PWD}/${IPYTHON_NAME}
packaged $IPYTHON_NAME 'pip install ipython' 'ipython'
# ./examples/mandelbrot
MANDELBROT_NAME="mandelbrot-${{ matrix.target }}.bin"
MANDELBROT_PATH=${PWD}/${MANDELBROT_NAME}
Expand All @@ -87,6 +89,8 @@ jobs:
echo "ALIENS_PATH=${ALIENS_PATH}" >> $GITHUB_OUTPUT
echo "TEXTUAL_NAME=${TEXTUAL_NAME}" >> $GITHUB_OUTPUT
echo "TEXTUAL_PATH=${TEXTUAL_PATH}" >> $GITHUB_OUTPUT
echo "IPYTHON_NAME=${IPYTHON_NAME}" >> $GITHUB_OUTPUT
echo "IPYTHON_PATH=${IPYTHON_PATH}" >> $GITHUB_OUTPUT
echo "MANDELBROT_NAME=${MANDELBROT_NAME}" >> $GITHUB_OUTPUT
echo "MANDELBROT_PATH=${MANDELBROT_PATH}" >> $GITHUB_OUTPUT
echo "MINESWEEPER_NAME=${MINESWEEPER_NAME}" >> $GITHUB_OUTPUT
Expand All @@ -104,6 +108,12 @@ jobs:
name: ${{ steps.build.outputs.TEXTUAL_NAME }}
path: ${{ steps.build.outputs.TEXTUAL_PATH }}

- name: Upload IPython
uses: actions/upload-artifact@v4
with:
name: ${{ steps.build.outputs.IPYTHON_NAME }}
path: ${{ steps.build.outputs.IPYTHON_PATH }}

- name: Upload mandelbrot
uses: actions/upload-artifact@v4
with:
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ packaged ./aliens 'pip install pygame' 'python -m pygame.examples.aliens'

Another one that you can try out is `pygame.examples.chimp`.

### IPython (console scripts)

Packages that expose shell scripts (like `ipython`) should also just work when
creating a package, and these scripts can be used as the startup command:

```bash
packaged ./ipython 'pip install ipython' 'ipython'
```

Now running `./ipython` runs a portable version of IPython!

## Local Development / Testing

To test and modify the package locally:
Expand Down
51 changes: 50 additions & 1 deletion src/packaged/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

MAKESELF_PATH = os.path.join(os.path.dirname(__file__), "makeself.sh")
DEFAULT_PYTHON_VERSION = "3.12"
PACKAGED_PYTHON_FOLDER_NAME = ".packaged_python"


class SourceDirectoryNotFound(Exception):
Expand Down Expand Up @@ -47,7 +48,7 @@ def create_package(
startup_script_name = "_packaged_startup.sh"
startup_script_path = os.path.join(source_directory, startup_script_name)

packaged_python_path = os.path.join(source_directory, ".packaged_python")
packaged_python_path = os.path.join(source_directory, PACKAGED_PYTHON_FOLDER_NAME)
if os.path.exists(packaged_python_path):
shutil.rmtree(packaged_python_path)

Expand Down Expand Up @@ -85,6 +86,54 @@ def create_package(

os.chmod(startup_script_path, 0o777)

# Patch console scripts, replacing the shebang with /usr/bin/env
for filename in os.listdir(python_bin_folder):
filepath = os.path.join(python_bin_folder, filename)
if not os.path.isfile(filepath):
continue

with open(filepath, "rb") as file:
first_two_bytes = file.read(2)
if first_two_bytes != b"#!":
continue

shebang_command = file.readline()
first_line = file.readline()
second_line = file.readline()
rest_of_file = file.read()

# Case 1: shebang points to packaged python
# File looks like this:
# #!/path/to/.packaged_python/python/bin/python3.12
# ... rest of python code
if PACKAGED_PYTHON_FOLDER_NAME.encode() in shebang_command:
# rewrite this file to have a `env python` shebang
with open(filepath, "wb") as file:
file.write(b"#!/usr/bin/env python\n")
file.write(first_line)
file.write(second_line)
file.write(rest_of_file)
# Case 2: shebang is /bin/sh, but the script is an `exec`
# with the shebang to packaged python.
# File looks like this:
# #!/bin/sh
# '''exec' /path/to/.packaged_python/python/bin/python3.12 "$0" "$@"
# ' '''
# ... rest of python code
elif (
first_line.startswith(b"'''exec' ")
and PACKAGED_PYTHON_FOLDER_NAME.encode() in first_line
):
# rewrite this file to have a `env python` shebang,
# and get rid of the first two lines as they're not needed
assert second_line == b"' '''\n"
with open(filepath, "wb") as file:
file.write(b"#!/usr/bin/env python\n")
file.write(rest_of_file)
first_line
else:
continue

# This uses `makeself` to build the binary
subprocess.check_call(
[
Expand Down
13 changes: 13 additions & 0 deletions tests/end_to_end/packaged_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,16 @@ def test_config_parsing() -> None:
config.startup_command,
):
assert "('R', 'G', 'B')\n('C', 'M', 'Y', 'K')" in get_output(config.output_path)


def test_console_scripts() -> None:
"""Packages `configtest` to ensure config parsing works as expected."""
package_path = os.path.join(TEST_PACKAGES, "console_script_test")
executable_path = "./zxpy.bin"
with build_package(
package_path,
executable_path,
"pip install zxpy",
"./script.zxpy",
):
assert "hello world!" in get_output(executable_path)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env zxpy
~"echo hello world!"

0 comments on commit 23cc255

Please sign in to comment.