1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-02 15:06:23 -07:00
gochan/build.py

546 lines
16 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Gochan build/install/maintenance script
For a list of commands, run
python3 build.py --help
For information on a specific command, run
python3 build.py <command> --help
See README.md for more info
"""
import argparse
import errno
import os
from os import path
from pathlib import Path
import shutil
import subprocess
import sys
import traceback
release_files = (
"html/css/",
"html/error/",
"html/static/",
"html/favicon2.png",
"html/favicon.png",
"html/firstrun.html",
"html/js/",
"examples/configs/",
"examples/plugins/",
"templates/",
"sql/initdb_mysql.sql",
"sql/initdb_postgres.sql",
"sql/initdb_sqlite3.sql",
"sql/reset_views.sql",
"sql/README.md",
"LICENSE",
"README.md",
)
GOCHAN_VERSION = "4.1.0"
DATABASE_VERSION = "5" # stored in DBNAME.DBPREFIXdatabase_version
PATH_NOTHING = -1
PATH_UNKNOWN = 0
PATH_FILE = 1
PATH_DIR = 2
PATH_LINK = 4
gcos = ""
gcos_name = "" # used for release, since macOS GOOS is "darwin"
exe = ""
gochan_bin = ""
gochan_exe = ""
migration_bin = ""
migration_exe = ""
def pathinfo(loc):
i = PATH_UNKNOWN
if not path.exists(loc):
return PATH_NOTHING
if path.islink(loc):
i |= PATH_LINK
if path.isfile(loc):
i |= PATH_FILE
elif path.isdir(loc):
i |= PATH_DIR
else:
i = PATH_UNKNOWN
return i
def delete(delpath):
"""
Deletes the given file, link, or directory and silently fails if nothing exists.
Returns the path info as well
"""
pinfo = pathinfo(delpath)
if pinfo == PATH_NOTHING:
return PATH_NOTHING
if pinfo & PATH_FILE > 0 or pinfo & PATH_LINK > 0:
os.remove(delpath)
return pinfo
if pinfo & PATH_DIR > 0:
shutil.rmtree(delpath)
return pinfo
return PATH_UNKNOWN
def mkdir(dirpath, force=False):
if path.exists(dirpath):
if force:
delete(dirpath)
else:
return
os.makedirs(dirpath)
def copy(source, dest):
"""
Copy source to dest, overwriting dest if source and dest are files, and merging
them if source is a directory and dest is a directory that already exists, overwriting
any conflicting files
"""
srcinfo = pathinfo(source)
destinfo = pathinfo(dest)
if srcinfo == PATH_NOTHING:
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), source)
if srcinfo & PATH_FILE > 0 or srcinfo & PATH_LINK > 0:
shutil.copy(source, dest)
return
if srcinfo & PATH_DIR > 0:
if destinfo == PATH_NOTHING:
mkdir(dest)
else:
for root, dirs, files in os.walk(source):
mkdir(path.join(dest, root))
for dirpath in dirs:
mkdir(path.join(dest, root, dirpath))
for file in files:
shutil.copy(path.join(root, file), path.join(dest, root, file))
def symlink(target, link):
"""Create symbolic link at `link` that points to `target`"""
targetinfo = pathinfo(target)
linkinfo = pathinfo(link)
if target == PATH_NOTHING:
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), target)
if linkinfo != PATH_NOTHING:
delete(link)
elif linkinfo == PATH_DIR and targetinfo == PATH_FILE:
target = path.join(link, path.basename(target))
target = path.abspath(target)
print("Creating a symbolic link at", link, "pointing to", target)
Path(link).symlink_to(target)
def run_cmd(cmd, print_output=True, realtime=False, print_command=False):
if print_command:
print(" ".join(cmd))
proc = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
output = ""
status = 0
if realtime: # print the command's output in real time, ignores print_output
while True:
try:
realtime_output = proc.stdout.readline().decode("utf-8")
if realtime_output == "" and status is not None:
return ("", status)
if realtime_output:
print(realtime_output.strip())
output += realtime_output
status = proc.poll()
except KeyboardInterrupt:
return (output, 0)
else: # wait until the command is finished to print the output
output = proc.communicate()[0]
if output is not None:
output = output.decode("utf-8").strip()
else:
output = ""
status = proc.wait()
if output != "" and print_output:
print(output)
if status is None:
status = 0
return (output, status)
def set_vars(goos=""):
""" Sets GOOS-related variables to be used globally"""
global gcos
global gcos_name # used for release, since macOS GOOS is "darwin"
global exe
global gochan_bin
global gochan_exe
global migration_bin
global migration_exe
if goos != "":
os.environ["GOOS"] = goos
gcos, gcos_status = run_cmd(("go", "env", "GOOS"), print_output=False)
exe, exe_status = run_cmd(("go", "env", "GOEXE"), print_output=False)
if gcos_status + exe_status != 0:
print("Invalid GOOS value, check your GOOS environment variable")
sys.exit(1)
gcos_name = gcos
if gcos_name == "darwin":
gcos_name = "macos"
gochan_bin = "gochan"
gochan_exe = "gochan" + exe
migration_bin = "gochan-migration"
migration_exe = "gochan-migration" + exe
def build(debugging=False, plugin_path="", static_templates=False):
"""Build the gochan executable for the current GOOS"""
pwd = os.getcwd()
trimpath = f"-trimpath={pwd}"
gcflags_debug = " -l -N" if debugging else ""
gcflags = f"-gcflags={trimpath}{gcflags_debug}"
ldflags_debug = "" if debugging else " -w -s"
ldflags = f"-ldflags=-X main.versionStr={GOCHAN_VERSION} -X main.dbVersionStr={DATABASE_VERSION} {ldflags_debug}"
build_cmd_base = ["go", "build", "-v", "-trimpath", gcflags, ldflags]
if static_templates:
print("Building error pages from templates")
with open("templates/404.html", "r") as tmpl404:
tmpl404str = tmpl404.read().strip()
with open("html/error/404.html", "w") as page404:
page404.write(tmpl404str.format(GOCHAN_VERSION))
with open("templates/5xx.html", "r") as tmpl5xx:
tmpl5xxStr = tmpl5xx.read().strip()
with open("html/error/500.html", "w") as page500:
page500.write(tmpl5xxStr.format(version=GOCHAN_VERSION, title="Error 500: Internal Server error"))
with open("html/error/502.html", "w") as page502:
page502.write(tmpl5xxStr.format(version=GOCHAN_VERSION, title="Error 502: Bad gateway"))
if debugging:
print(f"Building for {gcos} with debugging symbols")
else:
print(f"Building for {gcos}")
status = -1
if plugin_path != "" and plugin_path is not None:
build_cmd_base += ["-buildmode=plugin", plugin_path]
status = run_cmd(build_cmd_base, realtime=True, print_command=True)[1]
if status != 0:
print(f"Failed building plugin at {plugin_path}, see output for details")
sys.exit(1)
print("Built plugin successfully")
return
gochan_build_cmd = build_cmd_base + ["-o", gochan_exe, "./cmd/gochan"]
status = run_cmd(gochan_build_cmd, realtime=True, print_command=True)[1]
if status != 0:
print("Failed building gochan, see command output for details")
sys.exit(1)
print("Built gochan successfully")
gochan_migrate_build_cmd = build_cmd_base + ["-o", migration_exe, "./cmd/gochan-migration"]
status = run_cmd(gochan_migrate_build_cmd, realtime=True, print_command=True)[1]
if status != 0:
print("Failed building gochan-migration, see command output for details")
sys.exit(1)
print("Built gochan-migration successfully")
def clean():
print("Cleaning up")
del_files = ("gochan", "gochan.exe", "gochan-migration", "gochan-migration.exe", "releases/")
for del_file in del_files:
delete(del_file)
def install(prefix="/usr", document_root="/srv/gochan", symlinks=False, js_only=False, css_only=False, templates_only=False):
if gcos == "windows":
print("Installation is not currently supported for Windows, use the respective directory created by running `python build.py release`")
sys.exit(1)
mkdir(document_root)
mkdir(path.join(prefix, "share/gochan"))
print("Creating symbolic links: ", symlinks)
start_dir = path.abspath(path.curdir)
done = False
if js_only is True:
# args contains --js, install the JavaScript files
os.chdir(path.join(start_dir,"html/"))
if symlinks:
symlink("js", path.join(document_root, "js"))
else:
copy("js/", document_root)
os.chdir(start_dir)
done = True
print("JavaScript files installed")
if css_only is True:
# args contains --js, install the CSS files
os.chdir(path.join(start_dir,"html/"))
if symlinks:
symlink("css/", path.join(document_root, "css"))
else:
copy("css/", document_root)
os.chdir(start_dir)
done = True
print("CSS files installed")
if templates_only is True:
# args contains --js, install the templates
os.chdir(start_dir)
if symlinks:
symlink("templates/", path.join(prefix, "share/gochan/templates"))
else:
copy("templates/", path.join(prefix, "share/gochan"))
mkdir(path.join(prefix, "share/gochan/templates/override/"))
done = True
print("Templates installed")
if done is True:
print("Done installing specific stuff")
return
mkdir("/etc/gochan")
mkdir("/var/log/gochan")
for file in release_files:
try:
if file.startswith("html/"):
trimmed = path.relpath(file, "html/")
os.chdir(path.join(start_dir, "html/"))
print("copying", trimmed,"to", path.join(document_root, trimmed))
copy(trimmed, document_root)
os.chdir(start_dir)
else:
os.chdir(start_dir)
copy(file, path.join(prefix, "share/gochan"))
mkdir(path.join(prefix, "share/gochan/templates/override/"))
except shutil.SameFileError as err:
print(err, "(skipping)")
except FileNotFoundError:
if file == "html/js/":
print("Missing html/js directory, this must be built before installation by running python3 build.py js, or mkdir html/js if you don't want JavaScript")
else:
traceback.print_exc()
sys.exit(1)
if path.exists(gochan_exe) is False:
build()
print("Installing", gochan_exe, "to", path.join(prefix, "bin", gochan_exe))
try:
if symlinks:
symlink(gochan_exe, path.join(prefix, "bin", gochan_exe))
else:
copy(gochan_exe, path.join(prefix, "bin", gochan_exe))
except shutil.SameFileError:
print(gochan_exe, "and", path.join(prefix, "bin", gochan_exe), "are the same file, skipping")
if path.exists(migration_exe) is False:
build()
print("Installing ", migration_exe, "to", path.join(prefix, "bin", migration_exe))
try:
if symlinks:
symlink(migration_exe, path.join(prefix, "bin", migration_exe))
else:
copy(migration_exe, path.join(prefix, "bin", migration_exe))
except shutil.SameFileError:
print(migration_exe, "and", path.join(prefix, "bin", migration_exe), "are the same file, skipping")
print(
"gochan was successfully installed. If you haven't already, you should copy\n",
"examples/configs/gochan.example.json to /etc/gochan/gochan.json (modify as needed)\n",
"You may also need to go to https://yourgochansite/manage/rebuildall to rebuild the javascript config")
if gcos == "linux":
print(
"If your Linux distribution has systemd, you will also need to run the following commands:\n",
"cp examples/configs/gochan-[mysql|postgresql|sqlite3].service /lib/systemd/system/gochan.service\n",
"systemctl daemon-reload\n",
"systemctl enable gochan.service\n",
"systemctl start gochan.service")
print("")
def js(watch=False):
print("Transpiling JS")
mkdir("html/js/")
delete("html/js/gochan.js")
delete("html/js/gochan.js.map")
npm_cmd = ["npm", "--prefix", "frontend/", "run", "watch-ts" if watch else "build-ts"]
status = run_cmd(npm_cmd, True, True, True)[1]
if status != 0:
print("JS transpiling failed with status", status)
sys.exit(status)
def eslint(fix=False):
print("Running eslint")
npm_cmd = ["npm", "--prefix", "frontend/", "run", "eslint"]
if fix:
npm_cmd += ["--fix"]
status = run_cmd(npm_cmd, True, True, True)[1]
if status != 0:
print("ESLint failed with status", status)
sys.exit(status)
def release(goos):
set_vars(goos)
build(False, static_templates=True)
release_name = gochan_bin + "-v" + GOCHAN_VERSION + "_" + gcos_name
release_dir = path.join("releases", release_name)
delete(release_dir)
print("Creating release for", gcos_name, "\n")
mkdir(path.join(release_dir, "html"))
mkdir(path.join(release_dir, "sql"))
for file in release_files:
srcinfo = pathinfo(file)
if srcinfo == PATH_NOTHING:
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), file)
if srcinfo & PATH_FILE > 0:
shutil.copy(file, path.join(release_dir, file))
if srcinfo & PATH_DIR > 0:
shutil.copytree(file, path.join(release_dir, file))
copy(gochan_exe, path.join(release_dir, gochan_exe))
copy(migration_exe, path.join(release_dir, migration_exe))
delete(path.join(release_dir, "html/js/consts.js"))
archive_type = "zip" if goos in ('windows', 'darwin') else "gztar"
shutil.make_archive(release_dir, archive_type, root_dir="releases", base_dir=release_name)
def sass(watch=False):
npm_cmd = ["npm", "--prefix", "frontend/", "run", "watch-sass" if watch else "build-sass"]
status = run_cmd(npm_cmd, True, True, True)[1]
if status != 0:
print("Failed running sass with status", status)
sys.exit(status)
def test(verbose=False, coverage=False):
cmd = ["go", "test"]
if verbose:
cmd += ["-v"]
if coverage:
cmd += ["-cover"]
cmd += ["./pkg/...", "./cmd/..."]
run_cmd(cmd, realtime=True, print_command=True)
if __name__ == "__main__":
action = "build"
try:
action = sys.argv.pop(1)
except IndexError: # no argument was passed
pass
if action.startswith("-") is False:
sys.argv.insert(1, action)
set_vars()
valid_actions = (
"build", "clean", "install", "js", "release", "sass", "test", "selenium"
)
parser = argparse.ArgumentParser(description="gochan build script")
parser.add_argument("action", nargs=1, default="build", choices=valid_actions)
if action in ('--help', '-h'):
parser.print_help()
sys.exit(2)
if action == "build":
parser.add_argument("--debug",
help="build gochan and gochan-migrate with debugging symbols",
action="store_true")
parser.add_argument("--plugin",
help="if used, builds the gochan-compatible Go plugin at the specified directory")
parser.add_argument("--static-templates",
help="if used, also (re)builds the static error page templates based on the current gochan version",
action="store_true")
args = parser.parse_args()
build(args.debug, args.plugin, args.static_templates)
elif action == "clean":
clean()
sys.exit(0)
elif action == "install":
parser.add_argument("--js",
action="store_true",
help="only install JavaScript (useful for frontend development)")
parser.add_argument("--css",
action="store_true",
help="only install CSS")
parser.add_argument("--templates",
action="store_true",
help="install the template files")
parser.add_argument("--prefix",
default="/usr",
help="install gochan to this directory and its subdirectories")
parser.add_argument("--documentroot",
default="/srv/gochan",
help="install files in ./html/ to this directory to be requested by a browser")
parser.add_argument("--symlinks",
action="store_true",
help="create symbolic links instead of copying the files (may require admin/root privileges)")
args = parser.parse_args()
install(args.prefix, args.documentroot, args.symlinks, args.js, args.css, args.templates)
elif action == "js":
parser.add_argument("--watch", "-w",
action="store_true",
help="automatically rebuild when you change a file (keeps running)")
parser.add_argument(
"--eslint",
action="store_true",
help="Run eslint on the JavaScript code to check for possible problems")
parser.add_argument("--eslint-fix",
action="store_true",
help="Run eslint on the JS code to try to fix detected problems")
args = parser.parse_args()
if args.eslint or args.eslint_fix:
eslint(args.eslint_fix)
else:
js(args.watch)
elif action == "release":
parser.add_argument("--all", "-a",
help="build releases for Windows, macOS, and Linux",
action="store_true")
args = parser.parse_args()
mkdir("releases")
if args.all:
release("windows")
release("darwin")
release("linux")
else:
release(gcos)
elif action == "sass":
parser.add_argument("--watch", "-w",
action="store_true",
help="automatically rebuild when you change a file (keeps running)")
args = parser.parse_args()
sass(args.watch)
elif action == "selenium":
from tools.selenium_testing.runtests import setup_selenium_args, start_tests, close_tests
args = setup_selenium_args(parser)
try:
start_tests(args.__dict__)
except KeyboardInterrupt:
print("Tests interrupted by KeyboardInterrupt, exiting")
except Exception:
traceback.print_exc()
close_tests()
elif action == "test":
parser.add_argument("--verbose","-v",
action="store_true",
help="Print log messages in the tests")
parser.add_argument("--coverage",
action="store_true",
help="Print unit test coverage")
args = parser.parse_args()
test(args.verbose, args.coverage)
args = parser.parse_args()