add gitignore
This commit is contained in:
1 parent
0d1ff65479
commit
f2c44dbc0d
8 files changed
+839
-77
No files matched your search
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
build.py - turns gitdesk.py into a single executable with PyInstaller.
|
||||
|
||||
Windows: double-click build.bat (or in a terminal: py -3 build.py)
|
||||
Linux / macOS: ./build.sh (or: python3 build.py)
|
||||
|
||||
Result: dist/gitdesk.exe on Windows, dist/gitdesk elsewhere. Build it on the system
|
||||
you'll run it on - PyInstaller can't make a Windows exe on Linux or the other way round.
|
||||
|
||||
Everything is logged to build.log next to this file. The throwaway build
|
||||
environment goes in a .build folder here - or, if this folder is inside OneDrive
|
||||
(whose syncing locks files mid-build), in %LOCALAPPDATA%\\gitdesk-build instead.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
IS_WIN = os.name == "nt"
|
||||
if IS_WIN and "onedrive" in HERE.lower() and os.environ.get("LOCALAPPDATA"):
|
||||
CACHE = os.path.join(os.environ["LOCALAPPDATA"], "gitdesk-build")
|
||||
else:
|
||||
CACHE = os.path.join(HERE, ".build")
|
||||
VENV = os.path.join(CACHE, "venv")
|
||||
VPY = os.path.join(VENV, "Scripts", "python.exe") if IS_WIN else os.path.join(VENV, "bin", "python")
|
||||
WORK = os.path.join(CACHE, "work")
|
||||
DIST = os.path.join(HERE, "dist")
|
||||
EXE = os.path.join(DIST, "gitdesk.exe" if IS_WIN else "gitdesk")
|
||||
LOG = os.path.join(HERE, "build.log")
|
||||
|
||||
_log = None
|
||||
|
||||
|
||||
def say(msg: str = "") -> None:
|
||||
try:
|
||||
print(msg, flush=True)
|
||||
except UnicodeEncodeError: # a console code page that can't show some character
|
||||
print(msg.encode("ascii", "replace").decode(), flush=True)
|
||||
if _log:
|
||||
_log.write(msg + "\n")
|
||||
_log.flush()
|
||||
|
||||
|
||||
def run(cmd, show=True):
|
||||
"""Run a command, echo and log its output. Returns (exit code, last lines)."""
|
||||
say("> " + " ".join(f'"{c}"' if " " in c else c for c in cmd))
|
||||
env = dict(os.environ, PYTHONUTF8="1", PYTHONIOENCODING="utf-8") # readable logs on any code page
|
||||
try:
|
||||
p = subprocess.Popen(cmd, cwd=HERE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env,
|
||||
stdin=subprocess.DEVNULL, text=True, encoding="utf-8", errors="replace")
|
||||
except OSError as e:
|
||||
say(f" couldn't start it: {e}")
|
||||
return 1, [str(e)]
|
||||
tail = []
|
||||
for line in p.stdout:
|
||||
_log.write(line)
|
||||
tail = (tail + [line.rstrip()])[-40:]
|
||||
if show:
|
||||
print(line, end="", flush=True)
|
||||
p.wait()
|
||||
_log.flush()
|
||||
return p.returncode, tail
|
||||
|
||||
|
||||
def fail(problem: str, hints=()) -> int:
|
||||
say()
|
||||
say("=" * 70)
|
||||
say("BUILD FAILED: " + problem)
|
||||
for h in hints:
|
||||
say(" * " + h)
|
||||
say()
|
||||
say(f"Everything that happened is in {LOG}")
|
||||
say("If the reason isn't clear, send that file over.")
|
||||
return 1
|
||||
|
||||
|
||||
def common_hints(text: str):
|
||||
hints = []
|
||||
low = text.lower()
|
||||
if "permission" in low or "access is denied" in low or "winerror 5" in low or "winerror 32" in low:
|
||||
hints.append("A file was locked. Close gitdesk if it's running, and close any window showing the "
|
||||
"dist folder. Antivirus or OneDrive syncing can also lock files - try again in a minute, "
|
||||
"or move this folder somewhere that isn't synced (e.g. C:\\dev\\gitdesk).")
|
||||
if "virus" in low or "winerror 225" in low or "potentially unwanted" in low:
|
||||
hints.append("Your antivirus blocked the new exe. Windows Defender sometimes flags PyInstaller "
|
||||
"builds by mistake: allow it in Windows Security > Protection history, or add the "
|
||||
"dist folder to the exclusions, then build again.")
|
||||
if "could not find a version" in low or "connection" in low or "ssl" in low or "proxy" in low \
|
||||
or "getaddrinfo" in low or "network" in low:
|
||||
hints.append("pip couldn't download packages - check the internet connection / VPN / proxy.")
|
||||
if "filename too long" in low or "path too long" in low or "winerror 206" in low:
|
||||
hints.append("A path got too long for Windows. Move this folder somewhere shorter, e.g. C:\\dev\\gitdesk.")
|
||||
if "no module named venv" in low or "ensurepip" in low:
|
||||
hints.append("This Python can't make virtual environments. On Debian/Ubuntu: sudo apt install python3-venv. "
|
||||
"On Windows, reinstall Python from python.org.")
|
||||
return hints
|
||||
|
||||
|
||||
def free_old_exe() -> bool:
|
||||
"""Make sure the old exe can be replaced (it can't be overwritten while it's running)."""
|
||||
if not os.path.exists(EXE):
|
||||
return True
|
||||
try:
|
||||
os.remove(EXE)
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
# Windows lets you rename a running exe even though you can't delete it.
|
||||
old = EXE + f".old-{int(time.time())}"
|
||||
try:
|
||||
os.replace(EXE, old)
|
||||
say(f"The old exe is still running, so it was renamed to {os.path.basename(old)} - delete it later.")
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def venv_works() -> bool:
|
||||
if not os.path.exists(VPY):
|
||||
return False
|
||||
try:
|
||||
return subprocess.run([VPY, "-c", "import sys, pip"], capture_output=True, timeout=60).returncode == 0
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
global _log
|
||||
try:
|
||||
_log = open(LOG, "w", encoding="utf-8")
|
||||
except OSError as e:
|
||||
print(f"Can't write {LOG}: {e}")
|
||||
return 1
|
||||
say(f"gitdesk build - Python {sys.version.split()[0]} ({sys.executable}) on {sys.platform}")
|
||||
say(f"Folder: {HERE}")
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
return fail("Python 3.9 or newer is needed.", ["Install a current Python from https://www.python.org/downloads/"])
|
||||
for name in ("gitdesk.py", "requirements.txt"):
|
||||
if not os.path.exists(os.path.join(HERE, name)):
|
||||
return fail(f"{name} isn't next to build.py.",
|
||||
["Put gitdesk.py, requirements.txt, build.py and build.bat in the same folder. If they "
|
||||
"came in a .zip, extract it first - building from inside a zip doesn't work."])
|
||||
|
||||
say("\n[1/4] Build environment")
|
||||
if not venv_works():
|
||||
if os.path.exists(VENV):
|
||||
say("The build environment is broken or from an old Python - recreating it.")
|
||||
shutil.rmtree(VENV, ignore_errors=True)
|
||||
os.makedirs(CACHE, exist_ok=True)
|
||||
rc, tail = run([sys.executable, "-m", "venv", VENV])
|
||||
if rc or not venv_works():
|
||||
return fail("Couldn't create the build environment.", common_hints("\n".join(tail)) or
|
||||
["Delete the folder " + CACHE + " and try again."])
|
||||
say(f"Using {VENV}")
|
||||
|
||||
say("\n[2/4] Installing PyInstaller and Textual (downloads them the first time)")
|
||||
run([VPY, "-m", "pip", "install", "--disable-pip-version-check", "--upgrade", "pip"], show=False)
|
||||
rc, tail = run([VPY, "-m", "pip", "install", "--disable-pip-version-check", "--upgrade",
|
||||
"pyinstaller", "-r", os.path.join(HERE, "requirements.txt")])
|
||||
if rc:
|
||||
return fail("pip couldn't install PyInstaller / Textual.", common_hints("\n".join(tail)) or
|
||||
["Check your internet connection, then try again.",
|
||||
"If it keeps failing, delete " + CACHE + " and try again."])
|
||||
if IS_WIN and subprocess.run([VPY, "-c", "import tkinter"], capture_output=True).returncode != 0:
|
||||
say("Note: this Python has no tkinter, so Choose... in the exe will use the older Windows folder "
|
||||
"dialog. To get the Explorer one: run the python.org installer again > Modify > tick "
|
||||
"'tcl/tk and IDLE'.")
|
||||
|
||||
say("\n[3/4] Building the executable (takes a minute)")
|
||||
if not free_old_exe():
|
||||
return fail(f"{EXE} is in use and can't be replaced.",
|
||||
["Close gitdesk (every window running it), then build again."])
|
||||
cmd = [VPY, "-m", "PyInstaller", "--noconfirm", "--clean", "--onefile", "--name", "gitdesk",
|
||||
# Textual and Rich load some of their modules on demand, so bundle all of them.
|
||||
"--collect-submodules", "textual", "--collect-data", "textual", "--collect-submodules", "rich",
|
||||
"--specpath", WORK, "--workpath", WORK, "--distpath", DIST,
|
||||
os.path.join(HERE, "gitdesk.py")]
|
||||
icon = os.path.join(HERE, "gitdesk.ico")
|
||||
if IS_WIN and os.path.exists(icon):
|
||||
cmd[-1:-1] = ["--icon", icon]
|
||||
rc, tail = run(cmd)
|
||||
if rc or not os.path.exists(EXE):
|
||||
return fail("PyInstaller couldn't build the executable.", common_hints("\n".join(tail)))
|
||||
|
||||
say("\n[4/4] Checking that the new executable starts")
|
||||
try:
|
||||
p = subprocess.run([EXE, "--selftest"], capture_output=True, text=True, timeout=180,
|
||||
stdin=subprocess.DEVNULL, encoding="utf-8", errors="replace")
|
||||
out = (p.stdout + p.stderr).strip()
|
||||
ok = p.returncode == 0 and "selftest ok" in out
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
out, ok = str(e), False
|
||||
_log.write(out + "\n")
|
||||
if not ok:
|
||||
say(out[-2000:])
|
||||
return fail("The executable was built but didn't start properly.", common_hints(out))
|
||||
|
||||
size = os.path.getsize(EXE) / 1024 / 1024
|
||||
say()
|
||||
say("=" * 70)
|
||||
say(f"Done: {EXE} ({size:.0f} MB)")
|
||||
say("Copy it anywhere (a folder on your PATH is handy) and run 'gitdesk' inside a repo,")
|
||||
say("or just double-click it.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
code = main()
|
||||
except KeyboardInterrupt:
|
||||
code = fail("Stopped.")
|
||||
except Exception as e: # never just vanish - always explain
|
||||
import traceback
|
||||
say(traceback.format_exc())
|
||||
code = fail(f"Unexpected error: {e}")
|
||||
sys.exit(code)
|
||||
Reference in new issue
Block a user