XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

Improve build

cb51c35e
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

18 个文件 +949 -347
Modified .gitignore +1 -1
@@ -16,4 +16,4 @@ g4f.exe
16 16 har_and_cookies
17 17 playground
18 18 .vscode
19 build
19
Modified g4f-go/Makefile +1 -2
@@ -2,7 +2,7 @@
2 2
3 3 all: fetch build
4 4
5 # Download & merge the CPython embeddable runtimes (needs curl/unzip/zip/python3)
5 # Fetch the runtime manifest (validates URLs; runtimes download at first run)
6 6 fetch:
7 7 ./fetch-python.sh
8 8
@@ -12,7 +12,6 @@ build:
12 12
13 13 clean:
14 14 rm -rf dist
15 find embed -name '*.zip' -delete
16 15
17 16 vet:
18 17 go vet ./...
Modified g4f-go/README.md +53 -69
@@ -1,8 +1,10 @@
1 1 # g4f-go
2 2
3 A single self-contained executable for [gpt4free](https://github.com/xtekky/gpt4free):
4 a small Go launcher with an embedded, relocatable CPython runtime that has
5 gpt4free pre-installed. No system Python required.
3 A small, self-contained Go launcher for [gpt4free](https://github.com/xtekky/gpt4free).
4 Unlike the original design, the CPython runtime is **not embedded** in the
5 binary: `g4f-go` is a few MB, and downloads the correct CPython for the host
6 platform on first run (with live progress feedback), then installs `g4f` into
7 it. No system Python required.
6 8
7 9 ```
8 10 g4f-go client "What is gpt4free?"
@@ -11,57 +13,40 @@ g4f-go api --port 8080
11 13
12 14 ## How it works
13 15
14 1. `fetch-python.sh` downloads the official CPython *embeddable* zips from
15 python.org for every supported platform, copies the `g4f` package into the
16 interpreter's `python-home/`, drops prebuilt wheels in `wheels/`, patches
17 `python311._pth` for relocation, and re-packs everything into
18 `embed/<os>/<os>-<arch>-embed-<ver>.zip`.
19 2. `go:embed` (build tags per OS) bakes that archive into the launcher binary.
20 3. On first run the launcher extracts the archive next to itself, finishes the
21 g4f install from the bundled wheels (offline), then execs
22 `python -m g4f <args>`.
23 4. `build-all.sh` cross-compiles the launcher with `CGO_ENABLED=0` for every
24 target and zips each release.
16 1. The binary embeds only a small manifest (`runtime.json`) that pins the
17 CPython archive URL, size, and sha256 per platform
18 (pbs install-only tarballs for desktop, the official python.org Android
19 package for Termux).
20 2. On first run `g4f-go` picks the entry for its host OS/arch, downloads the
21 archive into `~/.g4f/python-embed/` (or the app dir on Android), printing a
22 `\r` progress line (percent, bytes, throughput, ETA), verifies the sha256,
23 and extracts it.
24 3. It then bootstraps pip (`ensurepip`), installs `g4f[slim]` into the
25 interpreter, writes a `.installed` stamp, and finally runs your command.
26 4. Subsequent runs skip straight to step 3 — the runtime is cached until the
27 pinned version changes.
25 28
26 ```
27 ┌──────────────────────────────┐
28 │ g4f-go (Go, ~10 MB binary) │
29 │ ┌────────────────────────┐ │
30 │ │ CPython 3.14 runtime │ │ ← baked in via go:embed
31 │ │ + g4f + wheels │ │
32 │ └────────────────────────┘ │
33 └──────────────────────────────┘
34 │ first run: extract next to binary
35
36 g4f-go.exe/ + python-home/ + wheels/ + .g4f-runtime/.installed
37
38
39 exec python -m g4f <args>
40 ```
41
42 ## Build
29 Downloads go to:
43 30
44 Requires Go 1.22+ and (for `fetch-python.sh`) bash, curl, unzip, zip, python3.
31 | Platform | Location |
32 |---|---|
33 | Linux / macOS / Windows | `~/.g4f/python-embed/` |
34 | Android (Termux) | app-private dir (`G4F_ANDROID_FILES_DIR`, defaults to `$HOME/g4f-go-runtime`) |
45 35
46 ```bash
47 cd g4f-go
36 `G4F_PYTHON_ONLY=1 g4f-go --version` prints the downloaded interpreter path
37 without running gpt4free (useful for wrapping the runtime from other tools).
48 38
49 # 1. Download & embed the Python runtimes (all platforms, ~200 MB on disk)
50 ./fetch-python.sh
51 ./fetch-python.sh linux # or just one platform
39 ## Building
52 40
53 # 2. Build launchers for every OS/arch
54 ./build-all.sh # -> dist/g4f-go-<ver>-<os>-<arch>.zip
55 # or a single one:
56 go build -o g4f-go .
41 ```
42 go build -o g4f-go . # linux host build (fast iteration)
43 ./build-all.sh # cross-compile + zip releases for all targets
44 ./build-all.sh android # only the android target
45 ./fetch-python.sh # optional: re-pin sizes + sha256 in runtime.json
57 46 ```
58 47
59 Release zips live in `dist/`. Each zip is a portable folder: drop it anywhere,
60 run `g4f-go`, and the runtime is extracted next to it on first launch.
61
62 > **Size note:** CPython runtimes are ~40-60 MB per platform, so release zips
63 > are large (Windows ≈ 160 MB, Linux ≈ 820 MB). The `.zip` is stored inside the
64 > Go binary, and extracted on first run; the Go executable itself stays small.
48 The manifest is embedded via `go:embed runtime.json`; the binary builds
49 without network access.
65 50
66 51 ## Usage
67 52
@@ -69,40 +54,39 @@ run `g4f-go`, and the runtime is extracted next to it on first launch.
69 54 g4f-go <g4f args...> run gpt4free (e.g. g4f-go client "hello")
70 55 g4f-go api --port 8080 start the OpenAI-compatible API server
71 56 g4f-go gui launch the web GUI
72 g4f-go status show embedded runtime status
57 g4f-go status show runtime download/install status
73 58 g4f-go install g4f (re)install the g4f package (network)
74 59 g4f-go help show help
75 60 ```
76 61
77 Environment: `G4F_PYTHON_ONLY=1 g4f-go --version` prints the embedded python
78 path (useful for wrapping the runtime from other tools).
79
80 62 ## Supported platforms
81 63
82 | OS | Arch | Notes |
83 |----|------|-------|
84 | Linux | amd64, arm64 | official python.org x64 embed |
85 | Windows | amd64, x86 | official python.org win64/win32 embed |
86 | macOS | arm64 (universal2 embed), amd64 | universal2 zip runs on both |
87 | FreeBSD | amd64 | reuses the Linux x64 embed (Python-only, no C extensions) |
64 | OS | Arch | Runtime source |
65 |----|------|----------------|
66 | Linux | amd64, arm64 | python-build-standalone install-only tarball |
67 | Windows | amd64 | python-build-standalone install-only tarball |
68 | macOS | amd64, arm64 | python-build-standalone install-only tarball |
69 | Android | arm64 (Termux) | official python.org `*-linux-android` package |
88 70
89 `fetch-python.sh` can be extended to other OSes; add a row and a matching
90 `runtime_<os>.go` build-tagged file.
71 Android note: the python.org Android package ships `libpython3.14.so` +
72 stdlib but no `python` executable. `g4f-go` detects Termux
73 (`pm list packages`), merges the tarball into the app dir, and compiles a tiny
74 C runner with Termux's clang that `dlopen`s libpython — the same technique as
75 CPython's own android testbed.
91 76
92 ## Layout inside a release folder
77 ## Layout after first run (`~/.g4f/python-embed/`)
93 78
94 79 ```
95 g4f-go (binary)
96 python.exe / python (launcher)
97 python-home/ g4f package + site-packages
98 wheels/ prebuilt g4f dependency wheels
99 .g4f-runtime/.installed (stamp written on first run)
80 python-home/bin/python interpreter (pbs layout)
81 python-home/lib/python3.14 stdlib + site-packages (g4f installed here)
82 python (launcher) shell wrapper that sets PYTHONHOME/PYTHONPATH
83 .g4f-runtime/.runtime-ok stamp: download+extract complete
84 .g4f-runtime/.installed stamp: g4f pip-installed
100 85 ```
101 86
102 87 ## Limitations
103 88
104 - The runtime is extracted to disk next to the executable (this is what makes
105 it *relocatable* — CPython cannot run a 100%-in-memory interpreter reliably).
106 - Bundled dependency versions are pinned by `requirements-min.txt` at build
107 time; use `g4f-go install g4f` for network installs.
89 - The runtime is materialized on disk (CPython cannot run a 100%-in-memory
90 interpreter reliably); first run downloads ~50–800 MB depending on platform.
108 91 - macOS builds must be signed/notarized by the distributor for Gatekeeper.
92 - Android builds need Termux installed to compile the dlopen runner on device.
Modified g4f-go/build-all.sh +9 -11
@@ -2,13 +2,13 @@
2 2 # build-all.sh
3 3 #
4 4 # Cross-compiles the g4f-go launcher for every supported OS/arch and packs a
5 # release zip per platform. The embedded Python archive is expected to have
6 # been prepared by ./fetch-python.sh first (the launcher still builds without
7 # it, but then refuses to run).
5 # release zip per platform. The runtime is NOT embedded: it downloads from
6 # python.org / python-build-standalone on first run (see runtime.json).
7 # `./fetch-python.sh` only pins sizes+shas; the launcher builds without it.
8 8 #
9 9 # Usage:
10 10 # ./build-all.sh # build every target
11 # ./build-all.sh linux # build only one OS (linux|windows|darwin|bsd)
11 # ./build-all.sh linux # build only one OS (linux|windows|darwin|android)
12 12 # G4F_VERSION=0.1.0 ./build-all.sh # custom version
13 13
14 14 set -euo pipefail
@@ -19,13 +19,12 @@ VERSION="${G4F_VERSION:-0.1.0}"
19 19 OUT="${OUT:-$HERE/dist}"
20 20 mkdir -p "$OUT"
21 21
22 # Optional OS filter: maps the fetch-python.sh platform names (linux,
23 # windows, darwin, bsd) onto GOOS. Empty = build everything.
22 # Optional OS filter: maps the fetch-python.sh platform names onto GOOS.
24 23 WANT_OS="${1:-${OS_ONLY:-}}"
25 24 case "$WANT_OS" in
26 "linux"|"windows"|"darwin"|"bsd") echo "==> Building only: $WANT_OS" ;;
25 "linux"|"windows"|"darwin"|"android") echo "==> Building only: $WANT_OS" ;;
27 26 "") echo "==> Building all targets" ;;
28 *) echo "Unknown OS filter: $WANT_OS (expected linux|windows|darwin|bsd)" >&2; exit 1 ;;
27 *) echo "Unknown OS filter: $WANT_OS (expected linux|windows|darwin|android)" >&2; exit 1 ;;
29 28 esac
30 29
31 30 # os arch ext name
@@ -33,10 +32,9 @@ TARGETS=(
33 32 "linux amd64 g4f-go"
34 33 "linux arm64 g4f-go"
35 34 "windows amd64 g4f-go.exe"
36 "windows 386 g4f-go.exe"
37 35 "darwin arm64 g4f-go"
38 36 "darwin amd64 g4f-go"
39 "freebsd amd64 g4f-go"
37 "android arm64 g4f-go"
40 38 )
41 39
42 40 for t in "${TARGETS[@]}"; do
@@ -45,7 +43,7 @@ for t in "${TARGETS[@]}"; do
45 43 "linux") [[ "$GOOS" == "linux" ]] || continue ;;
46 44 "windows") [[ "$GOOS" == "windows" ]] || continue ;;
47 45 "darwin") [[ "$GOOS" == "darwin" ]] || continue ;;
48 "bsd") [[ "$GOOS" == "freebsd" ]] || continue ;;
46 "android") [[ "$GOOS" == "android" ]] || continue ;;
49 47 "") ;;
50 48 esac
51 49 echo "==> $GOOS/$GOARCH"
Added g4f-go/build/g4f-go-0.1.0-android-arm64.zip +0 -0
二进制文件已变更,无法进行逐行预览。
Added g4f-go/build/g4f-go-0.1.0-linux-amd64.zip +0 -0
二进制文件已变更,无法进行逐行预览。
Added g4f-go/build/g4f-go-0.1.0-linux-arm64.zip +0 -0
二进制文件已变更,无法进行逐行预览。
Added g4f-go/download.go +448 -0
@@ -0,0 +1,448 @@
1 package main
2
3 import (
4 "archive/tar"
5 "compress/gzip"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "fmt"
10 "io"
11 "net/http"
12 "os"
13 "path/filepath"
14 "runtime"
15 "strings"
16 "time"
17 )
18
19 // RuntimeManifest mirrors runtime.json: one download source per platform.
20 type RuntimeManifest struct {
21 Version int `json:"version"`
22 Python string `json:"python"`
23 PBSTag string `json:"pbs_tag"`
24 Platforms map[string]RuntimeSpec `json:"platforms"`
25 }
26
27 // RuntimeSpec is a single platform's runtime download.
28 type RuntimeSpec struct {
29 Kind string `json:"kind"` // "pbs" (install_only tarball) or "android"
30 Arch string `json:"arch"`
31 URL string `json:"url"`
32 Size int64 `json:"size"`
33 SHA256 string `json:"sha256"`
34 }
35
36 // runtimeManifestKey returns the runtime.json entry used for this host:
37 // "linux-x64", "linux-arm64", "windows-amd64", "darwin-*" or "android".
38 func runtimeManifestKey() string {
39 if runtime.GOOS == "android" {
40 return "android"
41 }
42 arch := goArchToken()
43 switch runtime.GOOS {
44 case "linux":
45 if arch == "arm64" {
46 return "linux-arm64"
47 }
48 return "linux-x64"
49 case "windows":
50 return "windows-amd64"
51 case "darwin":
52 if arch == "arm64" {
53 return "darwin-arm64"
54 }
55 return "darwin-x64"
56 }
57 return runtime.GOOS + "-" + arch
58 }
59
60 // runtimeSpecForHost picks the manifest entry for this OS/arch.
61 func runtimeSpecForHost(m *RuntimeManifest) (*RuntimeSpec, error) {
62 key := runtimeManifestKey()
63 spec, ok := m.Platforms[key]
64 if !ok {
65 return nil, fmt.Errorf("no runtime in manifest for platform %q", key)
66 }
67 return &spec, nil
68 }
69
70 // partName is the temp file used while a download is in flight.
71 func partName(binDir string) string {
72 return filepath.Join(binDir, ".g4f-runtime", "runtime.download")
73 }
74
75 // installedOkName marks a fully verified runtime extraction.
76 func installedOkName(binDir string) string {
77 return filepath.Join(binDir, ".g4f-runtime", ".runtime-ok")
78 }
79
80 // downloadRuntime fetches the manifest URL for this host into cachePath
81 // (verifying size + sha256 when pinned) and reports live progress to stderr.
82 func downloadRuntime(binDir, cachePath string, spec *RuntimeSpec) error {
83 if err := os.MkdirAll(filepath.Dir(partName(binDir)), 0o755); err != nil {
84 return err
85 }
86
87 // Cache is valid when file exists with the pinned size (or any size when
88 // the manifest has no pinned size/sha).
89 if fi, err := os.Stat(cachePath); err == nil && fi.Size() > 0 {
90 if spec.Size <= 0 || fi.Size() == spec.Size {
91 fmt.Printf("runtime: using cached download (%s)\n", humanBytes(fi.Size()))
92 return verifyRuntime(cachePath, spec)
93 }
94 fmt.Printf("runtime: cached download incomplete, re-downloading\n")
95 }
96
97 fmt.Printf("runtime: downloading CPython %s (%s)\n", "3.14.7", humanBytes(spec.Size))
98 fmt.Printf(" %s\n", spec.URL)
99 start := time.Now()
100
101 out, err := os.Create(partName(binDir))
102 if err != nil {
103 return err
104 }
105 // Network http.Client with no default timeout: progress is what keeps the
106 // user informed, not a hard cutoff.
107 client := &http.Client{}
108 resp, err := client.Get(spec.URL)
109 if err != nil {
110 out.Close()
111 os.Remove(partName(binDir))
112 return fmt.Errorf("download failed: %w", err)
113 }
114 defer resp.Body.Close()
115 if resp.StatusCode != http.StatusOK {
116 out.Close()
117 os.Remove(partName(binDir))
118 return fmt.Errorf("download failed: HTTP %s", resp.Status)
119 }
120
121 // Prefer Content-Length; fall back to manifest size.
122 total := resp.ContentLength
123 if total <= 0 {
124 total = spec.Size
125 }
126 _, copyErr := copyWithProgress(out, resp.Body, total, start)
127 if cerr := out.Close(); copyErr == nil {
128 copyErr = cerr
129 }
130 if copyErr != nil {
131 os.Remove(partName(binDir))
132 return fmt.Errorf("download interrupted: %w", copyErr)
133 }
134 if err := os.Rename(partName(binDir), cachePath); err != nil {
135 os.Remove(partName(binDir))
136 return err
137 }
138 fmt.Printf("runtime: downloaded %s in %s\n", humanBytes(total), time.Since(start).Round(time.Second))
139
140 if spec.Size > 0 {
141 fi, err := os.Stat(cachePath)
142 if err != nil {
143 return err
144 }
145 if fi.Size() != spec.Size {
146 return fmt.Errorf("size mismatch: got %d, expected %d (update runtime.json)", fi.Size(), spec.Size)
147 }
148 }
149 return verifyRuntime(cachePath, spec)
150 }
151
152 // verifyRuntime validates sha256 when pinned in the manifest.
153 func verifyRuntime(cachePath string, spec *RuntimeSpec) error {
154 if spec.SHA256 == "" {
155 return nil // unpinned; trust size/transport
156 }
157 f, err := os.Open(cachePath)
158 if err != nil {
159 return err
160 }
161 defer f.Close()
162 h := sha256.New()
163 if _, err := io.Copy(h, f); err != nil {
164 return err
165 }
166 got := hex.EncodeToString(h.Sum(nil))
167 if !strings.EqualFold(got, spec.SHA256) {
168 return fmt.Errorf("sha256 mismatch: got %s, want %s", got, spec.SHA256)
169 }
170 fmt.Println("runtime: sha256 verified")
171 return nil
172 }
173
174 // copyWithProgress streams r into w while printing a \r-updated progress bar.
175 func copyWithProgress(w io.Writer, r io.Reader, total int64, start time.Time) (int64, error) {
176 buf := make([]byte, 256*1024)
177 var written int64
178 lastPrint := time.Time{}
179 for {
180 n, err := r.Read(buf)
181 if n > 0 {
182 if _, werr := w.Write(buf[:n]); werr != nil {
183 return written, werr
184 }
185 written += int64(n)
186 // Throttle progress output to ~5 updates/sec.
187 if time.Since(lastPrint) > 200*time.Millisecond {
188 lastPrint = time.Now()
189 progressLine(written, total, start)
190 }
191 }
192 if err == io.EOF {
193 break
194 }
195 if err != nil {
196 return written, err
197 }
198 }
199 // Final line: clear the \r-update with a newline.
200 progressLine(written, total, start)
201 return written, nil
202 }
203
204 // progressLine prints "pct | done/total | rate | eta" on one line (carriage
205 // return, no newline) so it reads like a live progress bar.
206 func progressLine(written, total int64, start time.Time) {
207 if total > 0 {
208 pct := float64(written) / float64(total) * 100
209 eta := time.Duration(float64(time.Since(start)) / (float64(written) / float64(total)) * (1 - float64(written)/float64(total)))
210 fmt.Printf("\r %5.1f%% %s / %s %s/s eta %s",
211 pct, humanBytes(written), humanBytes(total), humanBytes(int64(float64(written)/time.Since(start).Seconds())), eta.Round(time.Second))
212 } else {
213 fmt.Printf("\r %s downloaded", humanBytes(written))
214 }
215 }
216
217 // humanBytes renders byte counts readably (KiB/MiB/GiB).
218 func humanBytes(b int64) string {
219 const unit = 1024
220 if b < unit {
221 return fmt.Sprintf("%d B", b)
222 }
223 div, exp := int64(unit), 0
224 for n := b / unit; n >= unit; n /= unit {
225 div *= unit
226 exp++
227 }
228 return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
229 }
230
231 // extractRuntime unpacks the downloaded archive into binDir/python-home/:
232 // - *.tar.gz (pbs, android) via gzip+tar with a single top dir stripped
233 // - *.zip (windows embed, legacy) via archive/zip
234 //
235 // The archive's single top-level directory (e.g. "python/", "prefix/") is
236 // stripped and the contents land in python-home/ (bin/..., lib/...),
237 // matching the launcher/lookup layout.
238 func extractRuntime(binDir, cachePath string) error {
239 dest := filepath.Join(binDir, "python-home")
240 if err := os.MkdirAll(dest, 0o755); err != nil {
241 return err
242 }
243 f, err := os.Open(cachePath)
244 if err != nil {
245 return err
246 }
247 defer f.Close()
248
249 if strings.HasSuffix(strings.ToLower(cachePath), ".zip") {
250 fi, err := f.Stat()
251 if err != nil {
252 return err
253 }
254 return extractZip(f, fi.Size(), dest)
255 }
256
257 gz, err := gzip.NewReader(f)
258 if err != nil {
259 return err
260 }
261 defer gz.Close()
262 tr := tar.NewReader(gz)
263
264 // Determine the single top-level directory.
265 var top string
266 for {
267 hdr, err := tr.Next()
268 if err == io.EOF {
269 break
270 }
271 if err != nil {
272 return err
273 }
274 clean := filepath.Clean(hdr.Name)
275 parts := strings.Split(clean, string(os.PathSeparator))
276 if len(parts) > 0 && parts[0] != "." && parts[0] != "" && top == "" {
277 top = parts[0]
278 }
279 }
280
281 // Second pass: extract everything with zip-slip protection.
282 if _, err := f.Seek(0, io.SeekStart); err != nil {
283 return err
284 }
285 gz, err = gzip.NewReader(f)
286 if err != nil {
287 return err
288 }
289 defer gz.Close()
290 tr = tar.NewReader(gz)
291 for {
292 hdr, err := tr.Next()
293 if err == io.EOF {
294 break
295 }
296 if err != nil {
297 return err
298 }
299 rel := hdr.Name
300 if top != "" {
301 rel = strings.TrimPrefix(rel, top+"/")
302 rel = strings.TrimPrefix(rel, top)
303 }
304 rel = strings.TrimPrefix(rel, "/")
305 rel = strings.TrimPrefix(rel, "./")
306 name := filepath.Clean(rel)
307 if name == "." || name == "" {
308 continue
309 }
310 if name == ".." || strings.HasPrefix(name, ".."+string(os.PathSeparator)) {
311 return fmt.Errorf("unsafe path in archive: %s", hdr.Name)
312 }
313 target := filepath.Join(dest, name)
314 if !strings.HasPrefix(target, filepath.Clean(dest)+string(os.PathSeparator)) {
315 return fmt.Errorf("unsafe path in archive: %s", hdr.Name)
316 }
317
318 switch hdr.Typeflag {
319 case tar.TypeDir:
320 if err := os.MkdirAll(target, 0o755); err != nil {
321 return err
322 }
323 case tar.TypeReg, tar.TypeRegA:
324 if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
325 return err
326 }
327 out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(hdr.Mode)&0o777)
328 if err != nil {
329 return err
330 }
331 if _, err := io.Copy(out, tr); err != nil {
332 out.Close()
333 return err
334 }
335 if err := out.Close(); err != nil {
336 return err
337 }
338 if err := os.Chmod(target, os.FileMode(hdr.Mode)&0o777); err != nil {
339 return err
340 }
341 case tar.TypeSymlink:
342 // Symlinks in pbs installs point within the tree; recreate them
343 // (libpython3.so -> libpython3.14.so etc).
344 if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
345 return err
346 }
347 _ = os.Remove(target)
348 if err := os.Symlink(hdr.Linkname, target); err != nil {
349 // Some filesystems disallow symlinks; fall back to a copy.
350 _ = os.Remove(target)
351 if copySymlinkTarget(target, hdr.Linkname) != nil {
352 // Best effort: log-free skip keeps extraction robust.
353 _ = err
354 }
355 }
356 default:
357 // Hard links, devices, etc: skip silently (not needed).
358 }
359 }
360 return nil
361 }
362
363 // copySymlinkTarget attempts to copy a symlink's target for filesystems that
364 // reject symlinks (best effort).
365 func copySymlinkTarget(target, linkname string) error {
366 src := filepath.Join(filepath.Dir(target), linkname)
367 data, err := os.ReadFile(src)
368 if err != nil {
369 return err
370 }
371 return os.WriteFile(target, data, 0o755)
372 }
373
374 // ensureRuntime is the top-level entry point. It downloads (if needed) and
375 // extracts the platform runtime into binDir, then returns the python
376 // executable/launcher path.
377 func ensureRuntime() (string, error) {
378 binDir := installDir()
379 exe, err := pythonExecutable(binDir)
380 if err == nil {
381 if _, statErr := os.Stat(exe); statErr == nil {
382 return exe, nil
383 }
384 }
385
386 manifest, err := readRuntimeManifest()
387 if err != nil {
388 return "", err
389 }
390 spec, err := runtimeSpecForHost(manifest)
391 if err != nil {
392 return "", err
393 }
394
395 cachePath := filepath.Join(binDir, ".g4f-runtime", "runtime-"+filepath.Base(spec.URL))
396 if err := downloadRuntime(binDir, cachePath, spec); err != nil {
397 return "", err
398 }
399
400 // Extract unless already done (stamp written after successful extract).
401 okStamp := installedOkName(binDir)
402 if _, err := os.Stat(okStamp); err != nil {
403 fmt.Println("runtime: extracting (this can take a minute)...")
404 start := time.Now()
405 if err := extractRuntime(binDir, cachePath); err != nil {
406 return "", fmt.Errorf("extract runtime: %w", err)
407 }
408 if err := os.WriteFile(okStamp, []byte("ok"), 0o644); err != nil {
409 return "", err
410 }
411 fmt.Printf("runtime: extracted in %s\n", time.Since(start).Round(time.Second))
412 }
413
414 return finalizeRuntime(binDir)
415 }
416
417 // finalizeRuntime does platform-specific finishing (launcher setup) and
418 // returns the interpreter path.
419 func finalizeRuntime(binDir string) (string, error) {
420 // Android builds the dlopen C runner instead of a shell launcher.
421 if runtime.GOOS == "android" {
422 return finalizeAndroidRuntime(binDir)
423 }
424 // Write the unix launcher (windows uses python.exe from the archive).
425 if err := writeLauncher(binDir); err != nil {
426 return "", err
427 }
428 exe, err := pythonExecutable(binDir)
429 if err != nil {
430 return "", err
431 }
432 if _, err := os.Stat(exe); err != nil {
433 return "", fmt.Errorf("python runtime extracted but %s is missing", exe)
434 }
435 return exe, nil
436 }
437
438 // parseRuntimeManifest decodes a RuntimeManifest from bytes.
439 func parseRuntimeManifest(data []byte) (*RuntimeManifest, error) {
440 var m RuntimeManifest
441 if err := json.Unmarshal(data, &m); err != nil {
442 return nil, err
443 }
444 if len(m.Platforms) == 0 {
445 return nil, fmt.Errorf("runtime.json: no platforms defined")
446 }
447 return &m, nil
448 }
Modified g4f-go/fetch-python.sh +55 -165
@@ -1,181 +1,71 @@
1 1 #!/usr/bin/env bash
2 2 # fetch-python.sh
3 3 #
4 # Downloads the CPython embeddable runtime for every supported platform,
5 # merges gpt4free into it (source + prebuilt wheels), patches python311._pth
6 # so the interpreter is fully relocatable, and stores the result as a zip
7 # next to embed/<os>/EMPTY_PYTHON_RUNTIME so `go:embed` picks it up.
4 # Downloads the CPython runtimes for every supported platform and pins their
5 # size + sha256 into runtime.json. The g4f-go binary itself does NOT embed
6 # the runtimes anymore: it downloads the matching archive on first run (with
7 # live progress feedback) and verifies it against this manifest.
8 8 #
9 9 # Usage:
10 # ./fetch-python.sh # all platforms (linux, windows, darwin, bsd)
11 # ./fetch-python.sh linux # single platform
12 # G4F_VERSION=0.4.x ./fetch-python.sh
10 # ./fetch-python.sh # download all platforms
11 # ./fetch-python.sh linux # single platform group (linux|windows|darwin|android)
13 12 #
14 # Requirements: bash, curl, unzip, zip, python3 (with pip)
13 # Requirements: bash, curl, python3 (for sha256/size pinning)
14 #
15 # After updating URLs in runtime.json, re-run this to refresh the pins.
15 16
16 17 set -euo pipefail
17 18
18 19 HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19 PYVER="3.14.7"
20 PYTAG="cp314"
21 PYABI="cp314-cp314"
22 PBS_TAG="${PBS_TAG:-20260805}"
23 PBS_BASE="https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_TAG}"
24 PYORG_BASE="https://www.python.org/ftp/python/${PYVER}"
25 G4F_SRC="${G4F_SRC:-$HERE/..}" # gpt4free repository root
26 G4F_VERSION="${G4F_VERSION:-$(cd "$G4F_SRC" && python3 -c 'import sys;sys.path.insert(0,"g4f");from version import __version__;print(__version__)' 2>/dev/null || echo 0.4.x)}"
27
28 # Minimum versions for deps that have ancient pure-python releases on PyPI.
29 # `pip download --platform <x>` considers py3-none-any wheels valid for any
30 # target, so an old/misbehaving resolver can pick e.g. aiohttp 0.13.1 (2015,
31 # predates async/await) which crashes the embedded CPython 3.14. Pinning
32 # floors here keeps the offline wheel set sane.
33 WHEEL_FLOORS="aiohttp>=3.8"
34
35 WORK="$(mktemp -d)"
36 trap 'rm -rf "$WORK"' EXIT
37
38 mkdir -p "$HERE/embed/linux" "$HERE/embed/windows" "$HERE/embed/darwin" "$HERE/embed/bsd"
39
40 # Build the g4f project wheel ONCE (pure python, platform-independent).
41 mkdir -p "$WORK/wheels-cache"
42 (cd "$G4F_SRC" && python3 -m pip wheel . -w "$WORK/wheels-cache" --no-deps -q 2>/dev/null || true)
20 cd "$HERE"
21 MANIFEST="runtime.json"
43 22
44 # platform source spec goos goarch name
45 PLATFORMS=(
46 "linux pbs x86_64-unknown-linux-gnu linux amd64 linux-x64"
47 "linux pbs aarch64-unknown-linux-gnu linux arm64 linux-arm64"
48 "windows pbs x86_64-pc-windows-msvc windows amd64 windows-amd64"
49 "windows pbs i686-pc-windows-msvc windows 386 windows-x86"
50 "windows pbs aarch64-pc-windows-msvc windows arm64 windows-arm64"
51 "darwin pbs aarch64-apple-darwin darwin arm64 darwin-arm64"
52 "darwin pbs x86_64-apple-darwin darwin amd64 darwin-x64"
53 "bsd pbs x86_64-unknown-linux-gnu freebsd amd64 bsd-amd64"
54 )
55 WANT=("$@"); [ ${#WANT[@]} -eq 0 ] && WANT=(linux windows darwin bsd)
56
57 do_platform() {
58 local plat src spec goos goarch name
59 read -r plat src spec goos goarch name <<<"$1"
60 [[ " ${WANT[*]} " == *" $plat "* ]] || return 0
61 echo "==> [$plat/$goarch] $name ($src)"
62
63 local dir="$WORK/$name"
64 mkdir -p "$dir"
65 local home="$dir/python-home"
66 if [ "$src" = "pbs" ]; then
67 local asset="cpython-${PYVER}+${PBS_TAG}-${spec}-install_only.tar.gz"
68 curl -fsSL -o "$dir/base.tar.gz" "$PBS_BASE/$asset"
69 mkdir -p "$home"
70 # python-build-standalone is a full installable layout (bin/ or top-level
71 # python.exe) that is already relocatable, so no _pth patching is needed.
72 tar -xzf "$dir/base.tar.gz" -C "$home" --strip-components=1
73 rm -f "$dir/base.tar.gz"
74 # On unix the entry point is `bin/python3`; on windows `python.exe`.
75 if [ -f "$home/bin/python3" ]; then
76 mv "$home/bin/python3" "$home/bin/python"
77 find "$home/bin" -maxdepth 1 -name 'python3.*' -exec rm -f {} +
78 fi
79 else
80 local basezip="python-${PYVER}-${spec}.zip"
81 curl -fsSL -o "$dir/base.zip" "$PYORG_BASE/$basezip"
82 ( cd "$dir" && unzip -oq "$basezip" )
83 # 1) Relocatable: strip the hard-coded prefix path, keep python314.zip.
84 local pth
85 pth=$(find "$dir" -maxdepth 1 -name 'python3*._pth' | head -1)
86 sed -i 's|^#import site|import site|' "$pth" || true
87 grep -v '^\.\./' "$pth" > "$dir/py.pth" || true
88 printf '\n..\\python-home\n..\\wheels\n' >> "$dir/py.pth"
89 mv "$dir/py.pth" "$pth"
90 fi
91
92 # 2) Download wheels for THIS target platform/arch only, then merge the
93 # g4f package + wheels so the interpreter is self-contained.
94 # pip --platform lets us fetch the right cp314 wheels from a single
95 # ubuntu runner for every target (no need to execute the target python).
96 local tag
97 case "$name" in
98 linux-x64) tag="manylinux2014_x86_64" ;;
99 linux-arm64) tag="manylinux2014_aarch64" ;;
100 windows-amd64) tag="win_amd64" ;;
101 windows-x86) tag="win32" ;;
102 windows-arm64) tag="win_arm64" ;;
103 darwin-arm64) tag="macosx_11_0_arm64" ;;
104 darwin-x64) tag="macosx_10_15_universal2" ;; # covers x86_64 + arm64
105 bsd-amd64) tag="manylinux2014_x86_64" ;;
106 *) echo " WARNING: no wheel tag for $name; runtime will need network on first run" >&2; tag="" ;;
107 esac
108 mkdir -p "$dir/wheels"
109 # if [ -n "$tag" ]; then
110 # # Windows pip can't read process-substitution FDs from its subprocess, so
111 # # write the version floors to a real file once per platform.
112 # local floors="$dir/wheels.floors"
113 # printf '%s\n' "$WHEEL_FLOORS" > "$floors"
114 # # Full dependency resolution (no --no-deps) so the runtime's offline
115 # # `pip install g4f` finds every transitive wheel it needs.
116 # # brotli is optional in g4f and has no cp314 wheel for win_arm64;
117 # # fall back to fetching everything else if a single dep is unavailable.
118 # if ! python3 -m pip download \
119 # -r "$G4F_SRC/requirements-min.txt" \
120 # --constraint "$floors" \
121 # --only-binary=:all: \
122 # --python-version "$PYVER" --implementation cp --abi "cp$(echo "$PYVER" | tr -d '.')" \
123 # --platform "$tag" \
124 # -d "$dir/wheels" -q 2>"$dir/pip.err"; then
125 # echo " WARNING: full wheel set for $name not available; retrying without brotli (optional dep)" >&2
126 # grep -v '^brotli$' "$G4F_SRC/requirements-min.txt" > "$dir/req-nobrotli.txt"
127 # python3 -m pip download \
128 # -r "$dir/req-nobrotli.txt" \
129 # --constraint "$floors" \
130 # --only-binary=:all: \
131 # --python-version "$PYVER" --implementation cp --abi "cp$(echo "$PYVER" | tr -d '.')" \
132 # --platform "$tag" \
133 # -d "$dir/wheels" -q 2>>"$dir/pip.err" || {
134 # echo " WARNING: wheel download for $name failed; runtime will need network on first run" >&2
135 # }
136 # fi
137 # # Belt-and-braces: drop any wheel whose Requires-Python metadata excludes
138 # # our interpreter. The --constraint above prevents this at resolve time;
139 # # this catches stale wheels (e.g. copied in from wheels-cache) and covers
140 # # ancient pure-python releases like aiohttp 0.13.1.
141 # python3 - "$dir/wheels" "$PYVER" <<'PY' || true
142 # import glob, os, sys, zipfile
143 # from packaging.specifiers import SpecifierSet
144 # from packaging.version import Version
145 # want_v = Version(sys.argv[2])
146 # for whl in glob.glob(os.path.join(sys.argv[1], "*.whl")):
147 # try:
148 # with zipfile.ZipFile(whl) as z:
149 # meta = next((n for n in z.namelist() if n.endswith(".dist-info/METADATA")), None)
150 # if not meta:
151 # continue
152 # txt = z.read(meta).decode("utf-8", "replace")
153 # rp = next((l.split(":", 1)[1].strip() for l in txt.splitlines()
154 # if l.lower().startswith("requires-python:")), None)
155 # if rp and not SpecifierSet(rp).contains(want_v):
156 # print(f" removing {os.path.basename(whl)} (Requires-Python {rp} excludes {sys.argv[2]})")
157 # os.remove(whl)
158 # except Exception as e:
159 # print(f" skip check {os.path.basename(whl)}: {e}")
160 # PY
161 # fi
23 pick_urls() {
24 python3 - "$MANIFEST" "$WANT" <<'PY'
25 import json, sys
26 m = json.load(open(sys.argv[1]))
27 want = sys.argv[2]
28 for name, spec in m["platforms"].items():
29 if want == "all" or name.startswith(want):
30 print(name, spec["url"])
31 PY
32 }
162 33
163 # 3) Merge g4f package + wheels so the interpreter is self-contained.
164 # rsync -a --exclude='.git' --exclude='g4f-go' --exclude='g4f.dev' --exclude='g4f.egg-info' \
165 # "$G4F_SRC/g4f" "$dir/python-home/g4f"
166 # rsync -a --exclude='.git' --exclude='g4f-go' --exclude='g4f.dev' --exclude='g4f.egg-info' \
167 # "$G4F_SRC/requirements-min.txt" "$dir/python-home/requirements-min.txt"
168 # cp -n "$WORK/wheels-cache/"*.whl "$dir/wheels/" 2>/dev/null || true
34 WANT="${1:-all}"
35 case "$WANT" in
36 linux|windows|darwin|android|all) ;;
37 *) echo "Unknown filter: $WANT (expected linux|windows|darwin|android|all)" >&2; exit 1 ;;
38 esac
169 39
170 # 4) Pre-stamp install so first run is instant.
171 mkdir -p "$dir/.g4f-runtime"
172 printf 'g4f %s embedded (CPython %s)\n' "$G4F_VERSION" "$PYVER" > "$dir/.g4f-runtime/.installed"
40 if ! command -v python3 >/dev/null; then
41 echo "python3 required for manifest pinning" >&2; exit 1
42 fi
173 43
174 # 5) Repack next to the placeholder so go:embed sees only one zip.
175 rm -f "$HERE/embed/$plat/${name}-embed-${PYVER}.zip"
176 ( cd "$WORK" && zip -qr "$HERE/embed/$plat/${name}-embed-${PYVER}.zip" "$name" )
177 echo " -> embed/$plat/${name}-embed-${PYVER}.zip"
44 pin() {
45 # pin <name> <url>: download, compute sha256+size, update runtime.json
46 local name url file sha size
47 name="$1"; url="$2"
48 file="$(mktemp)"
49 echo "==> [$name] downloading $url"
50 curl -fsSL -o "$file" "$url"
51 sha="$(sha256sum "$file" | cut -d' ' -f1)"
52 size="$(stat -c%s "$file")"
53 rm -f "$file"
54 echo " sha256=$sha"
55 echo " size=$size"
56 python3 - "$MANIFEST" "$name" "$sha" "$size" <<'PY'
57 import json, sys
58 path, name, sha, size = sys.argv[1:5]
59 m = json.load(open(path))
60 m["platforms"][name]["sha256"] = sha
61 m["platforms"][name]["size"] = int(size)
62 json.dump(m, open(path, "w"), indent=2, sort_keys=True)
63 print(" -> runtime.json updated for", name)
64 PY
178 65 }
179 66
180 for p in "${PLATFORMS[@]}"; do do_platform "$p"; done
181 echo "Done. Rebuild with ./build-all.sh (or: go build -o g4f-go .)"
67 pick_urls | while read -r name url; do
68 pin "$name" "$url"
69 done
70
71 echo "Done. Build with ./build-all.sh (or: go build -o g4f-go .)"
Modified g4f-go/main.go +24 -24
@@ -5,29 +5,29 @@ import (
5 5 "fmt"
6 6 "os"
7 7 "os/signal"
8 "syscall"
9 8 "path/filepath"
10 9 "strings"
10 "syscall"
11 11 "time"
12 12 )
13 13
14 // main handles the `g4f-go <command>` interface. Anything unknown is forwarded
15 // to the embedded g4f CLI (so `g4f-go client "hello"` just works).
14 // printHelp shows the g4f-go usage.
16 15 func printHelp() {
17 fmt.Printf(`g4f-go %s - gpt4free with embedded Python %s
16 fmt.Printf(`g4f-go %s - gpt4free with a downloaded CPython %s runtime
18 17
19 18 Usage:
20 19 g4f-go <g4f args...> run gpt4free (e.g. g4f-go client "hello")
21 20 g4f-go api --port 8080 start the OpenAI-compatible API server
22 21 g4f-go gui launch the web GUI
23 g4f-go install g4f (re)install the g4f package into the embedded runtime
24 g4f-go status show runtime status
22 g4f-go status show runtime download/install status
23 g4f-go install g4f (re)install the g4f package (network)
24 g4f-go bootstrap refresh the g4f package installation
25 g4f-go --version print version
25 26 g4f-go help show this help
26 g4f-go --version show version
27 27
28 Environment:
29 G4F_PYTHON_ONLY=1 print the embedded python path and exit (for wrappers)
30 `, Version, PythonVer)
28 The CPython runtime downloads on first run (with progress feedback) into
29 %s. Set G4F_PYTHON_ONLY=1 to print the interpreter path and exit.
30 `, Version, PythonVer, installDir())
31 31 }
32 32
33 33 func main() {
@@ -56,19 +56,21 @@ func runMain() int {
56 56 printHelp()
57 57 return 0
58 58 case "--version", "-v":
59 fmt.Printf("g4f-go %s (embedded CPython %s)\n", Version, PythonVer)
59 fmt.Printf("g4f-go %s (CPython %s)\n", Version, PythonVer)
60 60 return 0
61 }
62
63 switch args[0] {
61 64 case "status":
62 exe := pythonExecutable(binDir)
63 65 stamp := filepath.Join(binDir, ".g4f-runtime", ".installed")
64 66 fmt.Printf("binary dir: %s\n", binDir)
65 fmt.Printf("python: %s\n", exe)
66 if _, err := os.Stat(exe); err == nil {
67 fmt.Println("runtime: extracted")
67 fmt.Printf("python: %s\n", py)
68 if _, serr := os.Stat(py); serr == nil {
69 fmt.Println("runtime: downloaded & extracted")
68 70 } else {
69 fmt.Println("runtime: not extracted (will extract on first run)")
71 fmt.Println("runtime: not downloaded yet (will download on first run)")
70 72 }
71 if _, err := os.Stat(stamp); err == nil {
73 if _, serr := os.Stat(stamp); serr == nil {
72 74 fmt.Println("g4f: installed")
73 75 } else {
74 76 fmt.Println("g4f: not installed (will install on first run)")
@@ -76,18 +78,15 @@ func runMain() int {
76 78 code, err := runPython(ctx, py, []string{"--version"})
77 79 if err != nil {
78 80 fmt.Fprintln(os.Stderr, "g4f-go:", err)
79 return 1
80 81 }
81 82 return code
82 83 case "install", "uninstall":
83 84 if len(args) < 2 {
84 85 fmt.Fprintln(os.Stderr, "usage: g4f-go install g4f")
85 return 2
86 86 }
87 87 code, err := runPython(ctx, py, append([]string{"-m", "pip"}, args...))
88 88 if err != nil {
89 89 fmt.Fprintln(os.Stderr, "g4f-go:", err)
90 return 1
91 90 }
92 91 return code
93 92 case "bootstrap":
@@ -95,7 +94,6 @@ func runMain() int {
95 94 code, err := runPython(ctx, py, []string{"-m", "pip", "install", "--no-input", "g4f[slim]"}, pipEnv(binDir)...)
96 95 if err != nil {
97 96 fmt.Fprintln(os.Stderr, "g4f-go:", err)
98 return 1
99 97 }
100 98 return code
101 99 }
@@ -105,18 +103,20 @@ func runMain() int {
105 103 return 0
106 104 }
107 105
108 exe := pythonExecutable(binDir)
106 exe, err := pythonExecutable(binDir)
107 if err != nil {
108 fmt.Fprintln(os.Stderr, "g4f-go:", err)
109 return 1
110 }
109 111 start := time.Now()
110 112 if err := installG4F(binDir, exe, start); err != nil {
111 113 fmt.Fprintln(os.Stderr, "g4f-go:", err)
112 return 1
113 114 }
114 115
115 116 // Default: forward everything to the g4f module.
116 117 code, err := runPython(ctx, py, append([]string{"-m", "g4f"}, args...))
117 118 if err != nil {
118 119 fmt.Fprintln(os.Stderr, "g4f-go:", err)
119 return 1
120 120 }
121 121 return code
122 122 }
Added g4f-go/manifest_embedded.go +23 -0
@@ -0,0 +1,23 @@
1 package main
2
3 import (
4 _ "embed"
5 "fmt"
6 )
7
8 //go:embed runtime.json
9 var embeddedManifest []byte
10
11 func init() {
12 // Validate at startup: a malformed runtime.json would otherwise only
13 // surface on first download.
14 if _, err := parseRuntimeManifest(embeddedManifest); err != nil {
15 panic(fmt.Sprintf("g4f-go: embedded runtime.json is invalid: %v", err))
16 }
17 }
18
19 // readRuntimeManifest is overridden by the embedded copy so releases work
20 // without a runtime.json next to the binary.
21 func readRuntimeManifest() (*RuntimeManifest, error) {
22 return parseRuntimeManifest(embeddedManifest)
23 }
Modified g4f-go/process.go +31 -18
@@ -13,11 +13,13 @@ import (
13 13 )
14 14
15 15 // pythonLauncher is the shell wrapper that sets PYTHONHOME etc. and execs the
16 // embedded python. Written next to the binary during extraction.
16 // downloaded python. Written next to the binary after the runtime is ready.
17 // Note: it must never exec itself ($DIR/python is the wrapper); the real
18 // interpreter always lives at $DIR/python-home/bin/python on unix.
17 19 const pythonLauncher = `#!/bin/sh
18 # g4f-go launcher for the embedded CPython runtime.
20 # g4f-go launcher for the downloaded CPython runtime.
19 21 DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
20 if [ -x "$DIR/python" ]; then PY="$DIR/python"; else PY="$DIR/python-home/bin/python"; fi
22 PY="$DIR/python-home/bin/python"
21 23 export PYTHONHOME="$DIR/python-home"
22 24 export PYTHONNOUSERSITE=1
23 25 export PYTHONDONTWRITEBYTECODE=1
@@ -122,36 +124,47 @@ func extractZip(r io.ReaderAt, size int64, dest string) error {
122 124 return nil
123 125 }
124 126
125 // pythonExecutable returns the launcher (unix) or python.exe (windows) path.
127 // pythonExecutable returns the python binary (or launcher) path for a
128 // downloaded runtime, or an error when no interpreter is present yet.
126 129 //
127 // The embedded archive is laid out as <name>/python-home/<exe>, so after
128 // extraction the interpreter lives at binDir/python-home/python.exe on
129 // windows and binDir/python-home/bin/python on unix. We prefer that location
130 // and fall back to the legacy binDir/python(.exe) layout produced by older
131 // archives, mirroring the shell launcher's logic.
132 func pythonExecutable(binDir string) string {
130 // pbs installs land in binDir/python-home/ with bin/python (unix) or
131 // python.exe (windows). The android build overrides this with a C runner
132 // that dlopens libpython (see runtime_android.go). Note that binDir/python
133 // is the *shell wrapper* (never a real interpreter), so it is not a valid
134 // fallback here.
135 func pythonExecutable(binDir string) (string, error) {
136 if exe, err := androidPythonExecutable(binDir); exe != "" || err != nil {
137 return exe, err
138 }
133 139 home := filepath.Join(binDir, "python-home")
140 var candidates []string
134 141 if runtime.GOOS == "windows" {
135 return filepath.Join(home, "python.exe")
142 candidates = []string{filepath.Join(home, "python.exe")}
143 } else {
144 candidates = []string{filepath.Join(home, "bin", "python")}
136 145 }
137 exe := filepath.Join(home, "bin", "python")
138 if fi, err := os.Stat(exe); err == nil && !fi.IsDir() {
139 return exe
146 for _, exe := range candidates {
147 if fi, err := os.Stat(exe); err == nil && !fi.IsDir() {
148 return exe, nil
149 }
140 150 }
141 return filepath.Join(binDir, "python")
151 return "", fmt.Errorf("no python executable found (runtime not downloaded?)")
142 152 }
143 153
144 // pythonHome returns the extracted interpreter root.
154 // pythonHome returns the downloaded interpreter root.
145 155 func pythonHome(binDir string) string {
156 if h := androidPythonHome(binDir); h != "" {
157 return h
158 }
146 159 return filepath.Join(binDir, "python-home")
147 160 }
148 161
149 // writeLauncher installs the unix shell wrapper after extraction.
162 // writeLauncher installs the unix shell wrapper after the runtime is ready.
150 163 func writeLauncher(binDir string) error {
151 164 if runtime.GOOS == "windows" {
152 165 return nil
153 166 }
154 path := pythonExecutable(binDir)
167 path := filepath.Join(binDir, "python")
155 168 if err := os.WriteFile(path, []byte(pythonLauncher), 0o755); err != nil {
156 169 return err
157 170 }
Modified g4f-go/runtime.go +31 -51
Added g4f-go/runtime.json +50 -0
@@ -0,0 +1,50 @@
1 {
2 "version": 1,
3 "python": "3.14.7",
4 "pbs_tag": "20260805",
5 "comment": "Runtime archives are downloaded at first run (never embedded in the binary). pbs = python-build-standalone (astral-sh), pyorg = python.org. SHA256 listed as empty must be filled after downloading (curl ... | sha256sum) and re-pinned.",
6 "platforms": {
7 "android": {
8 "kind": "android",
9 "arch": "any",
10 "url": "https://www.python.org/ftp/python/3.14.7/python-3.14.7-aarch64-linux-android.tar.gz",
11 "size": 22670000,
12 "sha256": ""
13 },
14 "linux-x64": {
15 "kind": "pbs",
16 "arch": "amd64",
17 "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.14.7+20260805-x86_64-unknown-linux-gnu-install_only.tar.gz",
18 "size": 0,
19 "sha256": ""
20 },
21 "linux-arm64": {
22 "kind": "pbs",
23 "arch": "arm64",
24 "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.14.7+20260805-aarch64-unknown-linux-gnu-install_only.tar.gz",
25 "size": 0,
26 "sha256": ""
27 },
28 "windows-amd64": {
29 "kind": "pbs",
30 "arch": "amd64",
31 "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.14.7+20260805-x86_64-pc-windows-msvc-install_only.tar.gz",
32 "size": 0,
33 "sha256": ""
34 },
35 "darwin-x64": {
36 "kind": "pbs",
37 "arch": "amd64",
38 "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.14.7+20260805-x86_64-apple-darwin-install_only.tar.gz",
39 "size": 0,
40 "sha256": ""
41 },
42 "darwin-arm64": {
43 "kind": "pbs",
44 "arch": "arm64",
45 "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.14.7+20260805-aarch64-apple-darwin-install_only.tar.gz",
46 "size": 0,
47 "sha256": ""
48 }
49 }
50 }
Added g4f-go/runtime_android.go +196 -0
Added g4f-go/runtime_stubs.go +18 -0
Modified g4f/Provider/DeepInfra.py +8 -5
@@ -173,6 +173,7 @@ class DeepInfra(OpenaiTemplate):
173 173 url = "https://deepinfra.com"
174 174 login_url = "https://deepinfra.com/dash/api_keys"
175 175 base_url = "https://api.deepinfra.com/v1/openai"
176 backup_url = "https://api.deepinfra.com/v1/openai"
176 177
177 178 working = True
178 179 active_by_default = True
@@ -210,7 +211,7 @@ class DeepInfra(OpenaiTemplate):
210 211 async def create_async_generator(
211 212 cls, model, messages, api_key=None, headers=None, **kwargs
212 213 ):
213 if not api_key:
214 if not api_key or not cls.is_provider_api_key(api_key):
214 215 # Generate a Turnstile token for each request (required without an API key)
215 216 token = await get_turnstile_token_async(model)
216 217 if token:
@@ -231,9 +232,11 @@ class DeepInfra(OpenaiTemplate):
231 232 def get_headers(
232 233 cls, stream: bool, api_key: str = None, headers: dict = None
233 234 ) -> dict:
234 headers = super().get_headers(stream, api_key, headers)
235 if not api_key:
236 headers["X-Deepinfra-Source"] = "web-page"
235 if not api_key or not cls.is_provider_api_key(api_key):
236 if headers is None:
237 headers = {}
238 headers["X-DeepInfra-Source"] = "web-page"
237 239 headers["Origin"] = "https://deepinfra.com"
238 240 headers["Referer"] = "https://deepinfra.com/"
239 return headers
241 api_key = None
242 return super().get_headers(stream, api_key, headers)
Modified g4f/Provider/template/OpenaiTemplate.py +1 -1
@@ -336,7 +336,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
336 336 return {
337 337 "Accept": "text/event-stream" if stream else "application/json",
338 338 "Content-Type": "application/json",
339 **({"Authorization": f"Bearer {api_key}"} if api_key else {}),
339 # **({"Authorization": f"Bearer {api_key}"} if api_key else {}),
340 340 **({} if headers is None else headers),
341 341 }
342 342