package main
import (
"bytes"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
// embedEmptyPlaceholder is the name of an empty placeholder file embedded per
// platform. It lets build tags select the right archive while keeping go:embed
// happy even before fetch-python.sh has run.
const embedEmptyPlaceholder = "EMPTY_PYTHON_RUNTIME"
// embedRuntimeArchive embeds one platform's runtime archive. The actual
// archive is generated by fetch-python.sh and stored under embed/<os>/.
// The variable is declared in the build-tagged files runtime_<os>.go.
// The placeholder file lets `go build` succeed before fetch-python.sh runs.
// installDir is the fixed user-level runtime location: ~/.g4f/python-embed.
// This keeps the executable fully relocatable and shares one runtime across
// all g4f-go binaries/versions on the machine. On Android it is overridden
// to the app's own writable directory (see runtime_android.go).
func installDir() string {
if d := androidInstallDir(); d != "" {
return d
}
home, err := os.UserHomeDir()
if err != nil {
home = "."
}
return filepath.Join(home, ".g4f", "python-embed")
}
// hasEmbeddedZip reports whether a real runtime archive is baked into this build.
func hasEmbeddedZip() bool {
entries, err := embeddedArchives()
return err == nil && len(entries) > 0
}
// embeddedArchives lists the runtime zips embedded for the current GOOS.
func embeddedArchives() ([]string, error) {
osDir := "embed/" + runtime.GOOS
return fs.Glob(embedRuntimeArchive, osDir+"/*.zip")
}
// goArchToken maps runtime.GOARCH to the token used in archive names.
func goArchToken() string {
switch runtime.GOARCH {
case "arm64":
return "arm64"
case "amd64", "x86_64":
return "x64"
case "386", "x86":
return "x86"
case "arm":
return "arm"
}
return runtime.GOARCH
}
// pickArchive chooses the embedded zip that matches the host architecture.
// Falls back to a generic x64/amd64 archive (e.g. FreeBSD reuses linux-x64).
func pickArchive(entries []string) string {
tok := goArchToken()
for _, e := range entries {
if strings.Contains(e, tok) {
return e
}
}
for _, e := range entries {
if strings.Contains(e, "x64") || strings.Contains(e, "amd64") {
return e
}
}
return entries[0]
}
// extractEmbedded unpacks embed/<os>/<os>-<arch>-embed-<ver>.zip into the
// user install dir using stdlib archive/zip only (no external tool needed).
func extractEmbedded(binDir string) error {
// The embed.FS layout mirrors the repo: embed/<os>/<archive>.zip.
entries, err := embeddedArchives()
if err != nil || len(entries) == 0 {
return fmt.Errorf("no embedded runtime archive for this platform (run fetch-python.sh and rebuild)")
}
archive := pickArchive(entries)
src, err := embedRuntimeArchive.Open(archive)
if err != nil {
return fmt.Errorf("open embedded archive: %w", err)
}
data, err := io.ReadAll(src)
src.Close()
if err != nil {
return fmt.Errorf("read embedded archive: %w", err)
}
if err := os.MkdirAll(binDir, 0o755); err != nil {
return err
}
if err := extractZip(bytes.NewReader(data), int64(len(data)), binDir); err != nil {
return fmt.Errorf("extract embedded runtime: %w", err)
}
return nil
}
// g4fIsInstalled reports whether the g4f Python package is already installed
// in the downloaded runtime by checking the .installed stamp.
func g4fIsInstalled(binDir string) bool {
stamp := filepath.Join(binDir, ".g4f-runtime", ".installed")
_, err := os.Stat(stamp)
return err == nil
}
// installG4F installs the g4f Python package into the downloaded runtime and
// writes the .installed stamp. Uses the bundled pip (ensurepip wheels in
// pbs installs, bootstrapped via `python -m ensurepip`) so no network access
// is required after the runtime was downloaded.
func installG4F(binDir, exe string, start time.Time) error {
fmt.Printf("Installing gpt4free into the Python runtime...\n")
if err := ensurePip(binDir, exe); err != nil {
return err
}
code, err := runPython(noSignalCtx(), exe, []string{
"-m", "pip", "install",
"--no-input", "g4f[slim]",
}, pipEnv(binDir)...)
if err != nil || code != 0 {
return fmt.Errorf("pip install g4f failed (exit %d): %w", code, err)
}
stamp := filepath.Join(binDir, ".g4f-runtime", ".installed")
if err := os.MkdirAll(filepath.Dir(stamp), 0o755); err != nil {
return err
}
if err := os.WriteFile(stamp, []byte("g4f installed"), 0o644); err != nil {
return err
}
fmt.Printf("gpt4free installed in %.1fs\n", time.Since(start).Seconds())
return nil
}
// upgradeG4F upgrades the g4f Python package in the downloaded runtime.
// Called on subsequent runs (after the initial install) when the user passes
// a subcommand that benefits from the latest version (api, gui, dev).
func upgradeG4F(binDir, exe string, start time.Time) error {
fmt.Printf("Upgrading gpt4free...\n")
if err := ensurePip(binDir, exe); err != nil {
return err
}
code, err := runPython(noSignalCtx(), exe, []string{
"-m", "pip", "install",
"--no-input", "--upgrade", "g4f[slim]",
}, pipEnv(binDir)...)
if err != nil || code != 0 {
return fmt.Errorf("pip upgrade g4f failed (exit %d): %w", code, err)
}
fmt.Printf("gpt4free upgraded in %.1fs\n", time.Since(start).Seconds())
return nil
}
// ensurePip makes `python -m pip` available in the downloaded runtime.
// pbs installs ship ensurepip but no standalone pip; bootstrap once.
func ensurePip(binDir, exe string) error {
code, err := runPython(noSignalCtx(), exe, []string{"-c", "import pip"}, pipEnv(binDir)...)
if err == nil && code == 0 {
return nil
}
fmt.Println(" bootstrapping pip (ensurepip)...")
code, err = runPython(noSignalCtx(), exe, []string{"-m", "ensurepip", "--upgrade"}, pipEnv(binDir)...)
if err != nil || code != 0 {
return fmt.Errorf("ensurepip failed (exit %d): %w", code, err)
}
return nil
}
// pipEnv restricts pip to the downloaded runtime so installs never touch the
// host Python. pbs installs are a full layout: lib/pythonX.Y/site-packages
// (unix) or Lib/site-packages (windows) inside the interpreter home.
func pipEnv(binDir string) []string {
home := pythonHome(binDir)
lib := filepath.Join(home, "Lib", "site-packages")
if runtime.GOOS != "windows" {
lib = filepath.Join(home, "lib", "python3.14", "site-packages")
}
return []string{
"PYTHONHOME=" + home,
"PYTHONNOUSERSITE=1",
"PYTHONDONTWRITEBYTECODE=1",
"PYTHONUTF8=1",
"PYTHONPATH=" + lib,
}
}
package main
import (
"bytes"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
// embedEmptyPlaceholder is the name of an empty placeholder file embedded per
// platform. It lets build tags select the right archive while keeping go:embed
// happy even before fetch-python.sh has run.
const embedEmptyPlaceholder = "EMPTY_PYTHON_RUNTIME"
// embedRuntimeArchive embeds one platform's runtime archive. The actual
// archive is generated by fetch-python.sh and stored under embed/<os>/.
// The variable is declared in the build-tagged files runtime_<os>.go.
// The placeholder file lets `go build` succeed before fetch-python.sh runs.
// installDir is the fixed user-level runtime location: ~/.g4f/python-embed.
// This keeps the executable fully relocatable and shares one runtime across
// all g4f-go binaries/versions on the machine. On Android it is overridden
// to the app's own writable directory (see runtime_android.go).
func installDir() string {
if d := androidInstallDir(); d != "" {
return d
}
home, err := os.UserHomeDir()
if err != nil {
home = "."
}
return filepath.Join(home, ".g4f", "python-embed")
}
// hasEmbeddedZip reports whether a real runtime archive is baked into this build.
func hasEmbeddedZip() bool {
entries, err := embeddedArchives()
return err == nil && len(entries) > 0
}
// embeddedArchives lists the runtime zips embedded for the current GOOS.
func embeddedArchives() ([]string, error) {
osDir := "embed/" + runtime.GOOS
return fs.Glob(embedRuntimeArchive, osDir+"/*.zip")
}
// goArchToken maps runtime.GOARCH to the token used in archive names.
func goArchToken() string {
switch runtime.GOARCH {
case "arm64":
return "arm64"
case "amd64", "x86_64":
return "x64"
case "386", "x86":
return "x86"
case "arm":
return "arm"
}
return runtime.GOARCH
}
// pickArchive chooses the embedded zip that matches the host architecture.
// Falls back to a generic x64/amd64 archive (e.g. FreeBSD reuses linux-x64).
func pickArchive(entries []string) string {
tok := goArchToken()
for _, e := range entries {
if strings.Contains(e, tok) {
return e
}
}
for _, e := range entries {
if strings.Contains(e, "x64") || strings.Contains(e, "amd64") {
return e
}
}
return entries[0]
}
// extractEmbedded unpacks embed/<os>/<os>-<arch>-embed-<ver>.zip into the
// user install dir using stdlib archive/zip only (no external tool needed).
func extractEmbedded(binDir string) error {
// The embed.FS layout mirrors the repo: embed/<os>/<archive>.zip.
entries, err := embeddedArchives()
if err != nil || len(entries) == 0 {
return fmt.Errorf("no embedded runtime archive for this platform (run fetch-python.sh and rebuild)")
}
archive := pickArchive(entries)
src, err := embedRuntimeArchive.Open(archive)
if err != nil {
return fmt.Errorf("open embedded archive: %w", err)
}
data, err := io.ReadAll(src)
src.Close()
if err != nil {
return fmt.Errorf("read embedded archive: %w", err)
}
if err := os.MkdirAll(binDir, 0o755); err != nil {
return err
}
if err := extractZip(bytes.NewReader(data), int64(len(data)), binDir); err != nil {
return fmt.Errorf("extract embedded runtime: %w", err)
}
return nil
}
// g4fIsInstalled reports whether the g4f Python package is already installed
// in the downloaded runtime by checking the .installed stamp.
func g4fIsInstalled(binDir string) bool {
stamp := filepath.Join(binDir, ".g4f-runtime", ".installed")
_, err := os.Stat(stamp)
return err == nil
}
// installG4F installs the g4f Python package into the downloaded runtime and
// writes the .installed stamp. Uses the bundled pip (ensurepip wheels in
// pbs installs, bootstrapped via `python -m ensurepip`) so no network access
// is required after the runtime was downloaded.
func installG4F(binDir, exe string, start time.Time) error {
fmt.Printf("Installing gpt4free into the Python runtime...\n")
if err := ensurePip(binDir, exe); err != nil {
return err
}
code, err := runPython(noSignalCtx(), exe, []string{
"-m", "pip", "install",
"--no-input", "g4f[slim]",
}, pipEnv(binDir)...)
if err != nil || code != 0 {
return fmt.Errorf("pip install g4f failed (exit %d): %w", code, err)
}
stamp := filepath.Join(binDir, ".g4f-runtime", ".installed")
if err := os.MkdirAll(filepath.Dir(stamp), 0o755); err != nil {
return err
}
if err := os.WriteFile(stamp, []byte("g4f installed"), 0o644); err != nil {
return err
}
fmt.Printf("gpt4free installed in %.1fs\n", time.Since(start).Seconds())
return nil
}
// upgradeG4F upgrades the g4f Python package in the downloaded runtime.
// Called on subsequent runs (after the initial install) when the user passes
// a subcommand that benefits from the latest version (api, gui, dev).
func upgradeG4F(binDir, exe string, start time.Time) error {
fmt.Printf("Upgrading gpt4free...\n")
if err := ensurePip(binDir, exe); err != nil {
return err
}
code, err := runPython(noSignalCtx(), exe, []string{
"-m", "pip", "install",
"--no-input", "--upgrade", "g4f[slim]",
}, pipEnv(binDir)...)
if err != nil || code != 0 {
return fmt.Errorf("pip upgrade g4f failed (exit %d): %w", code, err)
}
fmt.Printf("gpt4free upgraded in %.1fs\n", time.Since(start).Seconds())
return nil
}
// ensurePip makes `python -m pip` available in the downloaded runtime.
// pbs installs ship ensurepip but no standalone pip; bootstrap once.
func ensurePip(binDir, exe string) error {
code, err := runPython(noSignalCtx(), exe, []string{"-c", "import pip"}, pipEnv(binDir)...)
if err == nil && code == 0 {
return nil
}
fmt.Println(" bootstrapping pip (ensurepip)...")
code, err = runPython(noSignalCtx(), exe, []string{"-m", "ensurepip", "--upgrade"}, pipEnv(binDir)...)
if err != nil || code != 0 {
return fmt.Errorf("ensurepip failed (exit %d): %w", code, err)
}
return nil
}
// pipEnv restricts pip to the downloaded runtime so installs never touch the
// host Python. pbs installs are a full layout: lib/pythonX.Y/site-packages
// (unix) or Lib/site-packages (windows) inside the interpreter home.
func pipEnv(binDir string) []string {
home := pythonHome(binDir)
lib := filepath.Join(home, "Lib", "site-packages")
if runtime.GOOS != "windows" {
lib = filepath.Join(home, "lib", "python3.14", "site-packages")
}
return []string{
"PYTHONHOME=" + home,
"PYTHONNOUSERSITE=1",
"PYTHONDONTWRITEBYTECODE=1",
"PYTHONUTF8=1",
"PYTHONPATH=" + lib,
}
}