new options

This commit is contained in:
gru
2025-10-25 11:37:10 +02:00
parent c4c5259b07
commit 6473d1fa5f

View File

@@ -191,6 +191,40 @@ def comment_x_served_by_step(path="/etc/angie/conf.d/include/proxy.conf"):
print(f"✔ Hide X-Served-by header | backup: {backup}") print(f"✔ Hide X-Served-by header | backup: {backup}")
return count return count
def set_file_ownership(files: list[str | Path], owner: str, mode: int | None = None):
success = []
failed = []
for file_path in files:
path = Path(file_path)
if not path.exists():
failed.append((str(path), "File not found"))
continue
try:
run(["chown", owner, str(path)])
if mode is not None:
os.chmod(path, mode)
success.append(str(path))
except Exception as e:
failed.append((str(path), str(e)))
if success:
print(f"✔ Set ownership '{owner}' for {len(success)} file(s)")
if DEBUG:
for f in success:
print(f" - {f}")
if failed:
print(f"⚠ Failed to set ownership for {len(failed)} file(s):")
for f, err in failed:
print(f" - {f}: {err}")
return len(failed) == 0
def download_extract_tar_gz(url: str, dest_dir: Path) -> Path: def download_extract_tar_gz(url: str, dest_dir: Path) -> Path:
dest_dir.mkdir(parents=True, exist_ok=True) dest_dir.mkdir(parents=True, exist_ok=True)
@@ -757,8 +791,94 @@ def adjust_nginx_like_paths_in_tree(root: Path):
txt = txt.replace("daemon on;", "#daemon on;") txt = txt.replace("daemon on;", "#daemon on;")
cand.write_text(txt, encoding="utf-8") cand.write_text(txt, encoding="utf-8")
def install_node_and_yarn(node_pkg: str): def install_node_from_nodesource(version: str):
apt_install([node_pkg])
version_map = {
'latest': '22',
'lts': '22',
'current': '23'
}
node_version = version_map.get(version.lower(), version)
match = re.match(r'(\d+)', node_version)
if not match:
raise ValueError(f"Invalid Node.js version: {version}")
major_version = match.group(1)
with step("Removing old Node.js installations"):
run(["apt-get", "remove", "-y", "nodejs", "npm", "libnode-dev", "libnode72"], check=False)
run(["apt-get", "purge", "-y", "nodejs", "npm", "libnode-dev", "libnode72"], check=False)
run(["apt-get", "autoremove", "-y"], check=False)
for f in ["/etc/apt/sources.list.d/nodesource.list",
"/etc/apt/keyrings/nodesource.gpg",
"/usr/share/keyrings/nodesource.gpg",
"/etc/apt/trusted.gpg.d/nodesource.gpg"]:
if Path(f).exists():
Path(f).unlink()
with step(f"Installing Node.js v{major_version}.x from NodeSource repository"):
apt_try_install(["ca-certificates", "curl", "gnupg", "apt-transport-https"])
setup_url = f"https://deb.nodesource.com/setup_{major_version}.x"
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as tf:
script_path = tf.name
try:
run(["curl", "-fsSL", setup_url, "-o", script_path])
# Make it executable
os.chmod(script_path, 0o755)
if DEBUG:
subprocess.run(["bash", script_path], check=True)
else:
run(["bash", script_path])
run(["apt-get", "update", "-y"])
run(["apt-get", "install", "-y", "nodejs"])
finally:
if Path(script_path).exists():
os.unlink(script_path)
if shutil.which("node"):
node_ver = run_out(["node", "--version"], check=False).strip()
installed_major = re.match(r'v?(\d+)', node_ver)
if installed_major and installed_major.group(1) != major_version:
print(f"⚠ WARNING: Requested Node.js v{major_version}.x but got {node_ver}")
print(f" This likely means NodeSource doesn't support your distribution yet.")
if shutil.which("npm"):
npm_ver = run_out(["npm", "--version"], check=False).strip()
print(f"✔ Node.js {node_ver} | npm {npm_ver}")
else:
print(f"✔ Install npm")
run(["apt-get", "install", "-y", "npm"], check=False)
if not shutil.which("npm"):
run(["corepack", "enable"], check=False)
if shutil.which("npm"):
npm_ver = run_out(["npm", "--version"], check=False).strip()
print(f"✔ npm {npm_ver} installed successfully")
else:
print(f"✖ npm could not be installed - manual intervention required")
else:
print("✖ Node.js installation failed")
raise RuntimeError("Node.js installation failed")
def install_node_and_yarn(node_pkg: str = None, node_version: str = None):
if node_version:
install_node_from_nodesource(node_version)
else:
apt_install([node_pkg or "nodejs"])
if shutil.which("yarn") or shutil.which("yarnpkg"): if shutil.which("yarn") or shutil.which("yarnpkg"):
return return
apt_try_install(["yarn"]) apt_try_install(["yarn"])
@@ -831,65 +951,131 @@ def _prepare_sass(frontend_dir: Path):
def _build_frontend(src_frontend: Path, dest_frontend: Path): def _build_frontend(src_frontend: Path, dest_frontend: Path):
def _semver(s: str) -> bool: def _semver(s: str) -> bool:
return bool(re.match(r"^\d+(?:\.\d+){1,3}$", (s or "").strip())) return bool(re.match(r"^\d+(?:\.\d+){1,3}$", (s or "").strip()))
def _good_yarn(argv: list[str]) -> bool: def _good_yarn(argv: list[str]) -> bool:
try:
v = (run_out(argv + ["--version"], check=False) or "").strip() v = (run_out(argv + ["--version"], check=False) or "").strip()
return _semver(v) return _semver(v)
except Exception:
return False
def _pick_yarn_cmd() -> list[str] | None: def _pick_yarn_cmd() -> list[str] | None:
# Try direct yarn/yarnpkg first
for c in (["yarn"], ["yarnpkg"]): for c in (["yarn"], ["yarnpkg"]):
if shutil.which(c[0]) and _good_yarn(c): if shutil.which(c[0]) and _good_yarn(c):
return c return c
if shutil.which("npm") and (run_out(["npm", "--version"], check=False) or "").strip():
if _good_yarn(["npm", "exec", "--yes", "yarn@stable"]): # If npm exists, try to use it to run yarn
return ["npm", "exec", "--yes", "yarn@stable"] if shutil.which("npm"):
if shutil.which("npx") and (run_out(["npx", "--version"], check=False) or "").strip(): npm_ver = (run_out(["npm", "--version"], check=False) or "").strip()
if npm_ver:
# Try npm exec yarn@stable
if _good_yarn(["npm", "exec", "--yes", "yarn@stable", "--"]):
return ["npm", "exec", "--yes", "yarn@stable", "--"]
# Try npx as fallback
if shutil.which("npx"):
npx_ver = (run_out(["npx", "--version"], check=False) or "").strip()
if npx_ver:
if _good_yarn(["npx", "-y", "yarn@stable"]): if _good_yarn(["npx", "-y", "yarn@stable"]):
return ["npx", "-y", "yarn@stable"] return ["npx", "-y", "yarn@stable"]
return None return None
def _ensure_yarn_installed(): def _ensure_yarn_installed():
"""Install yarn globally using npm or corepack."""
with step("Installing yarn globally"):
if not shutil.which("npm"): if not shutil.which("npm"):
try: try:
apt_try_install(["npm"]) apt_try_install(["npm"])
except Exception: except Exception:
run(["apt-get", "update"], check=False) run(["apt-get", "update"], check=False)
run(["apt-get", "install", "-y", "npm"]) run(["apt-get", "install", "-y", "npm"])
run(["npm", "install", "-g", "yarn"], check=False)
# Try corepack first (modern way)
if shutil.which("corepack"):
try:
run(["corepack", "enable"])
run(["corepack", "prepare", "yarn@stable", "--activate"])
if shutil.which("yarn"):
return
except Exception:
pass
# Fallback to npm install
try:
run(["npm", "install", "-g", "yarn@latest"])
except Exception:
# Last resort - try with --force
run(["npm", "install", "-g", "--force", "yarn@latest"], check=False)
yarn_cmd = _pick_yarn_cmd() yarn_cmd = _pick_yarn_cmd()
if not yarn_cmd: if not yarn_cmd:
_ensure_yarn_installed() _ensure_yarn_installed()
yarn_cmd = _pick_yarn_cmd() yarn_cmd = _pick_yarn_cmd()
if not yarn_cmd: if not yarn_cmd:
raise RuntimeError("Unable to detect or install a valid Yarn. Try: apt-get install -y npm && npm i -g yarn.") raise RuntimeError(
"Unable to detect or install a valid Yarn.\n"
"Try manually: npm install -g yarn@latest"
)
with step("Installing frontend dependencies (yarn)"): with step("Installing frontend dependencies (yarn)"):
os.environ["NODE_ENV"] = "development" os.environ["NODE_ENV"] = "development"
os.chdir(src_frontend) os.chdir(src_frontend)
_prepare_sass(src_frontend) _prepare_sass(src_frontend)
# Get and create cache directory
try:
cache_dir = (run_out(yarn_cmd + ["cache", "dir"], check=False) or "").strip() cache_dir = (run_out(yarn_cmd + ["cache", "dir"], check=False) or "").strip()
if cache_dir and not Path(cache_dir).exists(): if cache_dir and not Path(cache_dir).exists():
Path(cache_dir).mkdir(parents=True, exist_ok=True) Path(cache_dir).mkdir(parents=True, exist_ok=True)
except Exception:
pass
# Clean cache
try:
run(yarn_cmd + ["cache", "clean"], check=False) run(yarn_cmd + ["cache", "clean"], check=False)
run(yarn_cmd + ["install"]) except Exception:
pass
install_cmd = yarn_cmd + ["install"]
if install_cmd[-1] == "--":
install_cmd = install_cmd[:-1]
if DEBUG:
print(f"Running: {' '.join(install_cmd)}")
try:
run(install_cmd)
except subprocess.CalledProcessError as e:
print(f"\n✖ Yarn install failed. Trying with --network-timeout and --ignore-engines...")
retry_cmd = install_cmd + ["--network-timeout", "100000", "--ignore-engines"]
run(retry_cmd)
with step("Building frontend (yarn build)"): with step("Building frontend (yarn build)"):
env = os.environ.copy() env = os.environ.copy()
env["NODE_OPTIONS"] = "--openssl-legacy-provider" env["NODE_OPTIONS"] = "--openssl-legacy-provider"
run(yarn_cmd + ["build"], env=env)
build_cmd = yarn_cmd + ["build"]
if build_cmd[-1] == "--":
build_cmd = build_cmd[:-1]
try:
run(build_cmd, env=env)
except subprocess.CalledProcessError:
print("\n⚠ Build failed with legacy provider, retrying without...")
env.pop("NODE_OPTIONS", None)
run(build_cmd, env=env)
with step("Copying frontend artifacts"): with step("Copying frontend artifacts"):
shutil.copytree(src_frontend / "dist", dest_frontend, dirs_exist_ok=True) shutil.copytree(src_frontend / "dist", dest_frontend, dirs_exist_ok=True)
if (src_frontend / "app-images").exists(): if (src_frontend / "app-images").exists():
shutil.copytree(src_frontend / "app-images", dest_frontend / "images", dirs_exist_ok=True) shutil.copytree(src_frontend / "app-images", dest_frontend / "images", dirs_exist_ok=True)
def patch_npm_backend_commands(): def patch_npm_backend_commands():
candidates = [ candidates = [
Path("/opt/npm/lib/utils.js"), Path("/opt/npm/lib/utils.js"),
@@ -1163,9 +1349,9 @@ def print_summary(info, ipv6_enabled, dark_enabled, update_mode):
# ========== UPDATE-ONLY ========== # ========== UPDATE-ONLY ==========
def update_only(node_pkg: str, npm_version_override: str | None, apply_dark: bool, dark_env: dict, ipv6_enabled: bool): def update_only(node_pkg: str, node_version: str | None, npm_version_override: str | None, apply_dark: bool, dark_env: dict, ipv6_enabled: bool):
apt_update_upgrade() apt_update_upgrade()
install_node_and_yarn(node_pkg) install_node_and_yarn(node_pkg=node_pkg, node_version=node_version)
version = github_latest_release_tag("NginxProxyManager/nginx-proxy-manager", npm_version_override) version = github_latest_release_tag("NginxProxyManager/nginx-proxy-manager", npm_version_override)
url = f"https://codeload.github.com/NginxProxyManager/nginx-proxy-manager/tar.gz/refs/tags/v{version}" url = f"https://codeload.github.com/NginxProxyManager/nginx-proxy-manager/tar.gz/refs/tags/v{version}"
@@ -1278,6 +1464,7 @@ def main():
formatter_class=argparse.ArgumentDefaultsHelpFormatter formatter_class=argparse.ArgumentDefaultsHelpFormatter
) )
parser.add_argument("--nodejs-pkg", default="nodejs", help="APT Node.js package name (e.g. nodejs, nodejs-18).") parser.add_argument("--nodejs-pkg", default="nodejs", help="APT Node.js package name (e.g. nodejs, nodejs-18).")
parser.add_argument("--node-version", default=None, help="Install Node.js from NodeSource repo (e.g. 'latest', '22', '20', '18'). Overrides --nodejs-pkg.")
parser.add_argument("--npm-version", default=None, help="Force NPM app version (e.g. 2.12.6). Default: latest release.") parser.add_argument("--npm-version", default=None, help="Force NPM app version (e.g. 2.12.6). Default: latest release.")
parser.add_argument("--motd", choices=["yes","no"], default="yes", help="Update MOTD after completion.") parser.add_argument("--motd", choices=["yes","no"], default="yes", help="Update MOTD after completion.")
parser.add_argument("--enable-ipv6", action="store_true", parser.add_argument("--enable-ipv6", action="store_true",
@@ -1310,6 +1497,7 @@ def main():
fix_logrotate_permissions_and_wrapper() fix_logrotate_permissions_and_wrapper()
version = update_only( version = update_only(
node_pkg=args.nodejs_pkg, node_pkg=args.nodejs_pkg,
node_version=args.node_version,
npm_version_override=args.npm_version, npm_version_override=args.npm_version,
apply_dark=args.dark_mode, apply_dark=args.dark_mode,
dark_env=dict( dark_env=dict(
@@ -1333,7 +1521,7 @@ def main():
setup_angie(ipv6_enabled=args.enable_ipv6) setup_angie(ipv6_enabled=args.enable_ipv6)
write_metrics_files() write_metrics_files()
install_node_and_yarn(args.nodejs_pkg) install_node_and_yarn(node_pkg=args.nodejs_pkg, node_version=args.node_version)
ensure_user_and_dirs() ensure_user_and_dirs()
create_sudoers_for_npm() create_sudoers_for_npm()
setup_certbot_venv() setup_certbot_venv()
@@ -1360,6 +1548,11 @@ def main():
fix_logrotate_permissions_and_wrapper() fix_logrotate_permissions_and_wrapper()
sync_backup_nginx_conf() sync_backup_nginx_conf()
comment_x_served_by_step() comment_x_served_by_step()
set_file_ownership(
["/etc/nginx/conf.d/include/ip_ranges.conf"],
"npm:npm",
0o664
)
with step("Restarting services after installation"): with step("Restarting services after installation"):
run(["systemctl","restart","angie.service"], check=False) run(["systemctl","restart","angie.service"], check=False)