From 6473d1fa5fe07b1d21b119d0ce0d8e7dc3042a73 Mon Sep 17 00:00:00 2001 From: gru Date: Sat, 25 Oct 2025 11:37:10 +0200 Subject: [PATCH] new options --- npm_install.py | 247 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 220 insertions(+), 27 deletions(-) diff --git a/npm_install.py b/npm_install.py index 1981ab3..cc8189e 100644 --- a/npm_install.py +++ b/npm_install.py @@ -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}") 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: 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;") cand.write_text(txt, encoding="utf-8") -def install_node_and_yarn(node_pkg: str): - apt_install([node_pkg]) +def install_node_from_nodesource(version: str): + + 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"): return apt_try_install(["yarn"]) @@ -831,65 +951,131 @@ def _prepare_sass(frontend_dir: Path): def _build_frontend(src_frontend: Path, dest_frontend: Path): - def _semver(s: str) -> bool: return bool(re.match(r"^\d+(?:\.\d+){1,3}$", (s or "").strip())) def _good_yarn(argv: list[str]) -> bool: - v = (run_out(argv + ["--version"], check=False) or "").strip() - return _semver(v) + try: + v = (run_out(argv + ["--version"], check=False) or "").strip() + return _semver(v) + except Exception: + return False def _pick_yarn_cmd() -> list[str] | None: + # Try direct yarn/yarnpkg first for c in (["yarn"], ["yarnpkg"]): if shutil.which(c[0]) and _good_yarn(c): return c - if shutil.which("npm") and (run_out(["npm", "--version"], check=False) or "").strip(): - if _good_yarn(["npm", "exec", "--yes", "yarn@stable"]): - return ["npm", "exec", "--yes", "yarn@stable"] - if shutil.which("npx") and (run_out(["npx", "--version"], check=False) or "").strip(): - if _good_yarn(["npx", "-y", "yarn@stable"]): - return ["npx", "-y", "yarn@stable"] + + # If npm exists, try to use it to run yarn + if shutil.which("npm"): + 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"]): + return ["npx", "-y", "yarn@stable"] + return None def _ensure_yarn_installed(): - if not shutil.which("npm"): + """Install yarn globally using npm or corepack.""" + with step("Installing yarn globally"): + if not shutil.which("npm"): + try: + apt_try_install(["npm"]) + except Exception: + run(["apt-get", "update"], check=False) + run(["apt-get", "install", "-y", "npm"]) + + # 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: - apt_try_install(["npm"]) + run(["npm", "install", "-g", "yarn@latest"]) except Exception: - run(["apt-get", "update"], check=False) - run(["apt-get", "install", "-y", "npm"]) - run(["npm", "install", "-g", "yarn"], check=False) + # Last resort - try with --force + run(["npm", "install", "-g", "--force", "yarn@latest"], check=False) yarn_cmd = _pick_yarn_cmd() if not yarn_cmd: _ensure_yarn_installed() yarn_cmd = _pick_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)"): os.environ["NODE_ENV"] = "development" os.chdir(src_frontend) _prepare_sass(src_frontend) - cache_dir = (run_out(yarn_cmd + ["cache", "dir"], check=False) or "").strip() - if cache_dir and not Path(cache_dir).exists(): - Path(cache_dir).mkdir(parents=True, exist_ok=True) + # Get and create cache directory + try: + cache_dir = (run_out(yarn_cmd + ["cache", "dir"], check=False) or "").strip() + if cache_dir and not Path(cache_dir).exists(): + Path(cache_dir).mkdir(parents=True, exist_ok=True) + except Exception: + pass - run(yarn_cmd + ["cache", "clean"], check=False) - run(yarn_cmd + ["install"]) + # Clean cache + try: + run(yarn_cmd + ["cache", "clean"], check=False) + 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)"): env = os.environ.copy() 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"): shutil.copytree(src_frontend / "dist", dest_frontend, dirs_exist_ok=True) if (src_frontend / "app-images").exists(): shutil.copytree(src_frontend / "app-images", dest_frontend / "images", dirs_exist_ok=True) - def patch_npm_backend_commands(): candidates = [ Path("/opt/npm/lib/utils.js"), @@ -1163,9 +1349,9 @@ def print_summary(info, ipv6_enabled, dark_enabled, update_mode): # ========== 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() - 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) 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 ) 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("--motd", choices=["yes","no"], default="yes", help="Update MOTD after completion.") parser.add_argument("--enable-ipv6", action="store_true", @@ -1310,6 +1497,7 @@ def main(): fix_logrotate_permissions_and_wrapper() version = update_only( node_pkg=args.nodejs_pkg, + node_version=args.node_version, npm_version_override=args.npm_version, apply_dark=args.dark_mode, dark_env=dict( @@ -1333,7 +1521,7 @@ def main(): setup_angie(ipv6_enabled=args.enable_ipv6) 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() create_sudoers_for_npm() setup_certbot_venv() @@ -1360,6 +1548,11 @@ def main(): fix_logrotate_permissions_and_wrapper() sync_backup_nginx_conf() 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"): run(["systemctl","restart","angie.service"], check=False)