Cinq fichiers employaient des f-strings PEP 701 — guillemets imbriqués du même type, backslash dans l'expression, expression sur plusieurs lignes — que Python refuse avant 3.12. Or les hooks de script/git/hooks portent « #!/usr/bin/env python3 » et tournent donc sur l'interpréteur du système, qu'une distribution livre encore en 3.10 : la SyntaxError tombe au chargement, sans qu'aucun garde puisse nommer la commande à taper. Les chaînes traduites gardent leur texte à l'octet près, sans quoi leur clé se perdrait. Les commentaires de ces fichiers passent du récit au présent, comme la règle le demande de ce qu'on touche. Vérifié : tout l'arbre Python parse sous 3.10, 3.11, 3.12 et 3.14. --- EN --- Five files used PEP 701 f-strings — quotes of the same kind nested, a backslash in the expression, an expression spanning lines — which Python refuses before 3.12. The hooks in script/git/hooks carry "#!/usr/bin/env python3" and so run on the system interpreter, which a distribution still ships as 3.10: the SyntaxError lands at load, before any guard can name the command to type. Translated strings keep their text byte for byte, else their key would be lost. The comments of those files move from tale to present tense, as the rule asks of what one touches. Checked: the whole Python tree parses under 3.10, 3.11, 3.12 and 3.14. Assisted-by: Claude Opus 5
197 lines
6.1 KiB
Python
Executable file
197 lines
6.1 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
import argparse
|
|
import os
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# « ExecStart=/bin/bash …/run.sh » et non le script seul.
|
|
#
|
|
# Lancé seul, systemd doit EXÉCUTER le fichier, et l'échoue en « 203/EXEC »
|
|
# dans quatre cas au moins : bit x absent, shebang qui ne résout pas, /home
|
|
# monté noexec, SELinux refusant l'execve. L'échec est alors MUET : le
|
|
# processus meurt avant d'entrer dans le script, sans une ligne de sortie, et
|
|
# 203 ressemble à une erreur d'application — le diagnostic part donc dans la
|
|
# mauvaise direction.
|
|
#
|
|
# Passé à bash, run.sh n'est plus qu'une DONNÉE lue : les quatre causes
|
|
# disparaissent ensemble, y compris noexec et SELinux, qui ne portent que sur
|
|
# l'execve. Rien n'est perdu au passage — le shebang du script désigne déjà
|
|
# bash. C'est aussi ce que font install_daemon.sh et le générateur de todo.py,
|
|
# les trois écrivant la même unité.
|
|
UNIT_TEMPLATE = """[Unit]
|
|
Description=ERPLibre for {user}
|
|
Requires=postgresql.service
|
|
After=network.target network-online.target postgresql.service
|
|
|
|
[Service]
|
|
Type=simple
|
|
SyslogIdentifier={user}
|
|
PermissionsStartOnly=true
|
|
User={user}
|
|
Group={user}
|
|
Restart=always
|
|
RestartSec=5
|
|
PIDFile={home_erplibre}/.venv.erplibre/service.pid
|
|
# bash explicite : voir install_daemon.py (evite 203/EXEC).
|
|
ExecStart=/bin/bash {home_erplibre}/run.sh{EXEC_PARAM}
|
|
WorkingDirectory={home_erplibre}
|
|
StandardOutput=journal+console
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
"""
|
|
|
|
|
|
def ensure_root():
|
|
if os.geteuid() != 0:
|
|
sys.exit("❌ This script must be run as root (use sudo or root).")
|
|
|
|
|
|
def run(cmd: list[str]) -> None:
|
|
subprocess.run(cmd, check=True)
|
|
|
|
|
|
def w_cmd(param: str, arg: str = None) -> str:
|
|
if not arg:
|
|
if type(arg) is str:
|
|
return ""
|
|
return param
|
|
return f"{param} {arg}"
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Create and enable a SystemD service for ERPLibre (Python equivalent of the bash script)."
|
|
)
|
|
|
|
# Exposed variables as configurable parameters
|
|
parser.add_argument(
|
|
"--user",
|
|
required=True,
|
|
help="Linux user running the service (e.g. erplibre)",
|
|
)
|
|
parser.add_argument(
|
|
"--home-erplibre",
|
|
help="Path to erplibre directory, workspace root path.",
|
|
)
|
|
parser.add_argument("--port", help="Application port (for information).")
|
|
parser.add_argument(
|
|
"--database",
|
|
help="Database name to run only with it (for information).",
|
|
)
|
|
parser.add_argument(
|
|
"--config-name",
|
|
help="Service name (and systemd file name without .service). Default: user",
|
|
)
|
|
|
|
# Utility flags
|
|
parser.add_argument(
|
|
"--no-enable",
|
|
action="store_true",
|
|
help="Do not enable service on boot.",
|
|
)
|
|
parser.add_argument(
|
|
"--no-restart",
|
|
action="store_true",
|
|
help="Do not restart the service after setup.",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Show what would be done without changing the system.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
ensure_root()
|
|
|
|
# Default paths and fallback values (same logic as in the bash script)
|
|
el_user = args.user
|
|
el_home_erplibre = args.home_erplibre or os.getcwd()
|
|
el_config_name = (
|
|
args.config_name
|
|
or f"erplibre_{el_user}_{os.path.basename(os.getcwd())}"
|
|
)
|
|
db_param = w_cmd("-d", args.database or "")
|
|
port_param = w_cmd("-p", args.port or "")
|
|
exec_param = " " + f" {db_param} {port_param}".strip()
|
|
|
|
# Render the systemd service file content
|
|
unit_content = UNIT_TEMPLATE.format(
|
|
user=el_user,
|
|
home_erplibre=el_home_erplibre,
|
|
EXEC_PARAM=exec_param,
|
|
)
|
|
|
|
unit_dest_path = Path(f"/etc/systemd/system/{el_config_name}.service")
|
|
|
|
print("* Final systemd file location:", unit_dest_path)
|
|
|
|
if args.dry_run:
|
|
print("\n--- GENERATED CONTENT ---\n")
|
|
print(unit_content)
|
|
print("\n--- END ---")
|
|
print("\n(dry-run) No files written, no systemctl commands executed.")
|
|
return
|
|
|
|
# Write temporary unit file
|
|
unit_dest_path.write_text(unit_content, encoding="utf-8")
|
|
|
|
# chmod 755
|
|
print("* Setting permissions to 755")
|
|
unit_dest_path.chmod(
|
|
stat.S_IRUSR
|
|
| stat.S_IWUSR
|
|
| stat.S_IXUSR
|
|
| stat.S_IRGRP
|
|
| stat.S_IXGRP
|
|
| stat.S_IROTH
|
|
| stat.S_IXOTH
|
|
)
|
|
|
|
# chown root:root
|
|
print("* Changing owner to root:root")
|
|
os.chown(unit_dest_path, 0, 0)
|
|
|
|
# Sanity checks
|
|
if not Path(el_home_erplibre).exists():
|
|
print(f"⚠️ Warning: {el_home_erplibre} does not exist.")
|
|
if not Path(el_home_erplibre, "run.sh").exists():
|
|
print(f"⚠️ Warning: {el_home_erplibre}/run.sh does not exist.")
|
|
|
|
# Reload systemd and enable service
|
|
print("* Reloading systemd daemon")
|
|
run(["systemctl", "daemon-reload"])
|
|
|
|
if not args.no_enable:
|
|
print(f"* Enabling service at boot: {el_config_name}.service")
|
|
run(["systemctl", "enable", f"{el_config_name}.service"])
|
|
else:
|
|
print("* (skip) enable step")
|
|
|
|
if not args.no_restart:
|
|
print(f"* Restarting service: {el_config_name}.service")
|
|
run(["systemctl", "restart", f"{el_config_name}.service"])
|
|
else:
|
|
print("* (skip) restart step")
|
|
|
|
# Summary (equivalent of bash echo block)
|
|
print("\n-----------------------------------------------------------")
|
|
print("Done! ERPLibre service created and started. Details:")
|
|
print(f"Port: {args.port}")
|
|
print(f"Service user: {el_user}")
|
|
print(f"Systemd file: {unit_dest_path}")
|
|
print(f"Start: systemctl start {el_config_name}")
|
|
print(f"Stop: systemctl stop {el_config_name}")
|
|
print(f"Restart: systemctl restart {el_config_name}")
|
|
print(f"Status: systemctl status {el_config_name}")
|
|
print(f"Logs: journalctl -feu {el_config_name}")
|
|
print("-----------------------------------------------------------")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|