Skip to content

Commit 0f51590

Browse files
committed
package binaries by default for linux, win, macOS
update readme examples based on testing. add scripts/package to build and package wheels. Signed-off-by: Bill Maxwell <[email protected]>
1 parent 2823d58 commit 0f51590

File tree

4 files changed

+227
-11
lines changed

4 files changed

+227
-11
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ wheels/
2323
*.egg-info/
2424
.installed.cfg
2525
*.egg
26+
bin/
2627

2728
# PyInstaller
2829
# Usually these files are written by a python script from a template

README.md

+26-9
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ You can install the GPTScript Python module using pip.
1212
pip install gptscript
1313
```
1414

15-
You will also want to have the tool download and install the gptscript cli by running:
15+
On MacOS, Windows X6
16+
17+
### SDIST and none-any wheel installations
18+
19+
When installing from the sdist or the none-any wheel, the binary is not packaged by default. You must run the install_gptscript command to install the binary.
1620

1721
```bash
1822
install_gptscript
@@ -27,6 +31,8 @@ from gptscript.install import install
2731
install()
2832
```
2933

34+
### Using an existing gptscript cli
35+
3036
If you already have the gptscript cli installed, you can use it by setting the envvar:
3137

3238
```bash
@@ -63,6 +69,17 @@ The `Tool` class represents a gptscript tool. The fields align with what you wou
6369

6470
Aside from the list methods there are `exec` and `exec_file` methods that allow you to execute a tool and get the responses. Those functions also provide a streaming version of execution if you want to process the output streams in your code as the tool is running.
6571

72+
### Opts
73+
74+
You can pass the following options to the exec and exec_file functions:
75+
76+
opts= {
77+
"cache": True(default)|False,
78+
"cache-dir": "",
79+
}
80+
81+
Cache can be set to true or false to enable or disable caching globally or it can be set at the individual tool level. The cache-dir can be set to a directory to use for caching. If not set, the default cache directory will be used.
82+
6683
### `list_models()`
6784

6885
This function lists the available GPT models.
@@ -85,7 +102,7 @@ tools = list_tools()
85102
print(tools)
86103
```
87104

88-
### `exec(tool)`
105+
### `exec(tool, opts)`
89106

90107
This function executes a tool and returns the response.
91108

@@ -115,9 +132,9 @@ response = exec(tool)
115132
print(response)
116133
```
117134

118-
### `exec_file(tool_path)`
135+
### `exec_file(tool_path, input="", opts)`
119136

120-
This function executes a tool from a file and returns the response.
137+
This function executes a tool from a file and returns the response. The input values are passed to the tool as args.
121138

122139
```python
123140
from gptscript.command import exec_file
@@ -126,12 +143,12 @@ response = exec_file("./example.gpt")
126143
print(response)
127144
```
128145

129-
### `stream_exec(tool)`
146+
### `stream_exec(tool, opts)`
130147

131148
This function streams the execution of a tool and returns the output, error, and process wait function. The streams must be read from.
132149

133150
```python
134-
from gptscript.command import stream_exec, complex_tool
151+
from gptscript.command import stream_exec
135152
from gptscript.tool import Tool
136153

137154
tool = Tool(
@@ -164,9 +181,9 @@ print_output(out, err)
164181
wait()
165182
```
166183

167-
### `stream_exec_file(tool_path)`
184+
### `stream_exec_file(tool_path, input="",opts)`
168185

169-
This function streams the execution of a tool from a file and returns the output, error, and process wait function.
186+
This function streams the execution of a tool from a file and returns the output, error, and process wait function. The input values are passed to the tool as args.
170187

171188
```python
172189
from gptscript.command import stream_exec_file
@@ -207,7 +224,7 @@ complex_tool = Tool(
207224
These should be descriptive and explain their point of view.
208225
Also come up with a made-up name, they each should be from different
209226
backgrounds and approach art differently.
210-
the response format should be:
227+
the JSON response format should be:
211228
{
212229
artists: [{
213230
name: "name"

pyproject.toml

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "gptscript"
7-
version = "0.1.0-rc1"
7+
version = "0.1.0-rc2"
88
description = "Run gptscripts from Python apps"
99
readme = "README.md"
1010
authors = [{ name = "Bill Maxwell", email = "[email protected]" }]
@@ -23,4 +23,6 @@ dependencies = [
2323
install_gptscript = "gptscript.install:install"
2424

2525
[tool.wheel]
26-
universal = true
26+
27+
[tool.setuptools]
28+
packages = ["gptscript"]

scripts/package

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#!/usr/bin/env python
2+
import os
3+
import sys
4+
import hashlib
5+
import requests
6+
import zipfile
7+
import tarfile
8+
import shutil
9+
import subprocess
10+
11+
from pathlib import Path
12+
from tqdm import tqdm
13+
from wheel.wheelfile import WheelFile
14+
from tempfile import TemporaryDirectory
15+
16+
# Define the base information for the download
17+
gptscript_info = {
18+
"name": "gptscript",
19+
"url": "https://github.com/gptscript-ai/gptscript/releases/download/",
20+
"version": "v0.1.4",
21+
}
22+
23+
# Define platform-specific variables
24+
platform_names = {
25+
"linux": {"name": "linux", "archs": ["arm64", "amd64"]},
26+
"darwin": {"name": "macOS", "archs": ["universal"]},
27+
"windows": {"name": "windows", "archs": ["amd64"]},
28+
}
29+
30+
# Define suffix for different platforms
31+
suffixes = {
32+
"linux": "tar.gz",
33+
"macOS": "tar.gz",
34+
"windows": "zip",
35+
}
36+
37+
def wheel_platform_tag(platform, arch):
38+
py_arch= {
39+
"universal": "universal2",
40+
"arm64": "aarch64",
41+
"amd64": "x86_64",
42+
}[arch]
43+
44+
py_platform = {
45+
"linux": "manylinux2014",
46+
"macOS":"macosx_10_9",
47+
"windows":"win",
48+
}[platform]
49+
50+
return f"{py_platform}_{py_arch}"
51+
52+
53+
def download_file(url, save_path):
54+
response = requests.get(url, stream=True)
55+
total_size = int(response.headers.get("content-length", 0))
56+
block_size = 1024
57+
progress_bar = tqdm(
58+
total=total_size,
59+
unit="B",
60+
unit_scale=True,
61+
desc=f"Downloading {url.split('/')[-1]}",
62+
)
63+
64+
with open(save_path, "wb") as f:
65+
for data in response.iter_content(block_size):
66+
progress_bar.update(len(data))
67+
f.write(data)
68+
69+
progress_bar.close()
70+
71+
72+
def extract_archive(archive_path, extract_dir):
73+
if archive_path.suffix == ".zip":
74+
with zipfile.ZipFile(archive_path, "r") as zip_ref:
75+
zip_ref.extractall(extract_dir)
76+
elif archive_path.suffixes[-2:] == [".tar", ".gz"]:
77+
with tarfile.open(archive_path, "r:gz") as tar_ref:
78+
tar_ref.extractall(extract_dir)
79+
80+
81+
def setup_directories(base_dir):
82+
for platform_name in platform_names.values():
83+
for arch in platform_name["archs"]:
84+
os.makedirs(base_dir / platform_name["name"] / arch, exist_ok=True)
85+
86+
87+
def stage_gptscript_binaries(base_dir):
88+
setup_directories(base_dir)
89+
90+
for platform_name in platform_names.values():
91+
for arch in platform_name["archs"]:
92+
pltfm_dir = platform_name["name"]
93+
file_suffix = suffixes[pltfm_dir]
94+
95+
artifact_name = f"{gptscript_info['name']}-{gptscript_info['version']}-{pltfm_dir}-{arch}.{file_suffix}"
96+
url = f"{gptscript_info['url']}{gptscript_info['version']}/{artifact_name}"
97+
98+
download_path = base_dir / pltfm_dir / arch / artifact_name
99+
download_file(url, download_path)
100+
101+
# Extract the downloaded archive
102+
extract_archive(download_path, base_dir / pltfm_dir / arch)
103+
104+
# Remove the archive file after extraction
105+
download_path.unlink()
106+
107+
108+
def build_wheel_for_platform(output_dir):
109+
"""
110+
Build a wheel for each platform specified in platform_names.
111+
Assumes a setup.py file is present and correctly configured.
112+
"""
113+
for platform in platform_names.values():
114+
for arch in platform["archs"]:
115+
# Set environment variables to influence the build process for specific platform and architecture
116+
os.environ["PLAT"] = f"{platform["name"]}-{arch}"
117+
try:
118+
# Call the build module to build the project
119+
subprocess.check_call(
120+
[sys.executable, "-m", "build", "--outdir", str(output_dir)]
121+
)
122+
finally:
123+
# Cleanup environment variables
124+
del os.environ["PLAT"]
125+
126+
def file_hash_and_size(file_path):
127+
"""Compute the SHA256 hash and size of a file."""
128+
sha256 = hashlib.sha256()
129+
size = 0
130+
with open(file_path, 'rb') as f:
131+
while True:
132+
data = f.read(8192)
133+
if not data:
134+
break
135+
sha256.update(data)
136+
size += len(data)
137+
return sha256.hexdigest(), f"{size}"
138+
139+
def modify_and_repackage_wheel(wheel_path, binary_dir, platform, arch):
140+
with TemporaryDirectory() as temp_dir:
141+
temp_dir_path = Path(temp_dir)
142+
with WheelFile(wheel_path, "r") as original_wheel:
143+
original_wheel.extractall(temp_dir_path)
144+
145+
# Identify the .dist-info directory
146+
dist_info_dirs = list(temp_dir_path.glob('*.dist-info'))
147+
if len(dist_info_dirs) != 1:
148+
raise RuntimeError("Expected exactly one .dist-info directory")
149+
dist_info_dir = dist_info_dirs[0]
150+
151+
# Perform necessary modifications here
152+
# Example: Adding a binary file to the .data directory
153+
binary_name = "gptscript" + (".exe" if platform == "windows" else "")
154+
binary_src = binary_dir / platform / arch / binary_name
155+
data_dir = temp_dir_path / dist_info_dir.name.replace('.dist-info', '.data') / 'scripts'
156+
data_dir.mkdir(parents=True, exist_ok=True)
157+
shutil.copy(binary_src, data_dir / binary_name)
158+
159+
# Update the RECORD file with new files
160+
record_path = dist_info_dir / "RECORD"
161+
with open(record_path, "a") as record_file:
162+
binary_rel_path = data_dir.relative_to(temp_dir_path) / binary_name
163+
hash_digest, size = file_hash_and_size(data_dir / binary_name)
164+
record_file.write(f"{binary_rel_path},{hash_digest},{size}\n")
165+
166+
platform_tag = "none-" + wheel_platform_tag(platform, arch)
167+
new_wheel_filename = wheel_path.name.replace("none-any", platform_tag)
168+
new_wheel_path = wheel_path.parent / new_wheel_filename
169+
170+
# Repackage the wheel, overwriting the original
171+
#new_wheel_path = wheel_path # Overwrite the original wheel or specify a new path
172+
with WheelFile(new_wheel_path, "w") as new_wheel:
173+
new_wheel.write_files(temp_dir_path)
174+
175+
print(f"Wheel modified and saved to: {new_wheel_path}")
176+
177+
178+
def main():
179+
base_dir = Path(__file__).resolve().parent
180+
bin_dir = base_dir.parent / "bin"
181+
dist_dir = base_dir.parent / "dist"
182+
183+
stage_gptscript_binaries(bin_dir)
184+
build_wheel_for_platform(dist_dir)
185+
186+
# Modify and repackage wheels
187+
wheel_files = list(dist_dir.glob("*.whl"))
188+
for _, data in platform_names.items():
189+
for arch in data["archs"]:
190+
# Find the wheel file (assuming there's only one for simplicity)
191+
if wheel_files:
192+
modify_and_repackage_wheel(wheel_files[0], bin_dir, data["name"], arch)
193+
194+
195+
if __name__ == "__main__":
196+
main()

0 commit comments

Comments
 (0)