« make version » lit .python-odoo-version et l'affichait sous le libellé nu « Python version ». Tant que les deux venvs partageaient leur interpréteur, le libellé suffisait ; depuis qu'ils divergent, il laisse croire que le dépôt n'a qu'une version de Python, et le chiffre affiché n'est pas celui du venv dans lequel l'utilisateur travaille. Les deux sont désormais nommées, celle d'Odoo comme celle de l'outillage. Seule la sortie change : cette valeur ne participe à aucun identifiant, et l'absence du fichier reste sans conséquence. Vérifié : les deux versions s'affichent, puis la conclusion reste la même. --- EN --- "make version" reads .python-odoo-version and printed it under the bare label "Python version". While both venvs shared their interpreter the label was enough; now that they diverge, it suggests the repository has a single Python version, and the figure shown is not that of the venv the user works in. Both are now named, Odoo's and the tooling's. Only the output changes: this value takes part in no identifier, and a missing file stays without consequence. Checked: both versions print, then the conclusion stays the same. Assisted-by: Claude Opus 5
894 lines
34 KiB
Python
Executable file
894 lines
34 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)
|
|
|
|
# This script need only basic importation, it needs to be supported by python of your system
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
# erplibre_state is in the same directory; import lazily to avoid hard failure
|
|
# when the script runs outside the project root (e.g. during bare install).
|
|
# Two import styles are needed: when this file is imported as a package module
|
|
# the repo root is on sys.path (package import works); when it is executed
|
|
# directly as ./script/version/update_env_version.py, sys.path[0] is
|
|
# script/version/ and only the sibling import resolves.
|
|
try:
|
|
from script.version.erplibre_state import (
|
|
get_version_extra,
|
|
get_version_installed,
|
|
print_state,
|
|
set_version_installed,
|
|
set_version_switched,
|
|
)
|
|
|
|
_STATE_AVAILABLE = True
|
|
except ImportError:
|
|
try:
|
|
from erplibre_state import (
|
|
get_version_extra,
|
|
get_version_installed,
|
|
print_state,
|
|
set_version_installed,
|
|
set_version_switched,
|
|
)
|
|
|
|
_STATE_AVAILABLE = True
|
|
except ImportError:
|
|
_STATE_AVAILABLE = False
|
|
|
|
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
PROJECT_NAME = os.path.basename(os.getcwd())
|
|
VERSION_DATA_FILE = os.path.join("conf", "supported_version_erplibre.json")
|
|
VERSION_PYTHON_FILE = os.path.join(".python-odoo-version")
|
|
INSTALLED_ODOO_VERSION_FILE = ".repo/installed_odoo_version.txt"
|
|
VERSION_ERPLIBRE_FILE = os.path.join(".erplibre-version")
|
|
VERSION_ODOO_FILE = os.path.join(".odoo-version")
|
|
VERSION_POETRY_FILE = os.path.join(".poetry-version")
|
|
# Le Python de l'OUTILLAGE, distinct de celui d'Odoo depuis que .venv.erplibre
|
|
# ne partage plus son interpréteur : il ne participe à aucun identifiant de
|
|
# version, il n'est là que pour être dit.
|
|
VERSION_PYTHON_ERPLIBRE_FILE = os.path.join("conf", "python-erplibre-version")
|
|
ADDONS_PATH = os.path.join("addons")
|
|
MOBILE_PATH = os.path.join("mobile", "erplibre_home_mobile")
|
|
VENV_TEMPLATE_FILE = ".venv.%s"
|
|
MANIFEST_FILE = "default.dev.xml"
|
|
MANIFEST_TEMPLATE_FILE = "default.dev.odoo%s.xml"
|
|
MANIFEST_FILE_PATH = os.path.join(".", "manifest", MANIFEST_FILE)
|
|
PYPROJECT_FILE = os.path.join("pyproject.toml")
|
|
PYPROJECT_TEMPLATE_FILE = "pyproject.%s.toml"
|
|
POETRY_LOCK_FILE = os.path.join("poetry.lock")
|
|
POETRY_LOCK_TEMPLATE_FILE = "poetry.%s.lock"
|
|
# PIP_REQUIREMENT_FILE = os.path.join("requirements.txt")
|
|
PIP_REQUIREMENT_TEMPLATE_FILE = "requirements.%s.txt"
|
|
PIP_IGNORE_REQUIREMENT_TEMPLATE_FILE = "ignore_requirements.%s.txt"
|
|
ADDONS_TEMPLATE_FILE = "odoo%s/addons"
|
|
ODOO_TEMPLATE_FILE = "odoo%s"
|
|
ERPLIBRE_TEMPLATE_VERSION = "odoo%s_python%s"
|
|
|
|
|
|
def get_config():
|
|
"""Parse command line arguments, extracting the config file name,
|
|
returning the union of config file and command line arguments
|
|
|
|
:return: dict of config file settings and command line arguments
|
|
"""
|
|
parser = argparse.ArgumentParser(
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
description=f"""\
|
|
Change environnement from supported version, check file {VERSION_DATA_FILE}
|
|
""",
|
|
epilog="""\
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"-l",
|
|
"--list",
|
|
action="store_true",
|
|
help="List all supported version.",
|
|
)
|
|
parser.add_argument(
|
|
"--python_version",
|
|
help="Select the python version.",
|
|
)
|
|
parser.add_argument(
|
|
"--poetry_version",
|
|
help="Select the poetry version.",
|
|
)
|
|
parser.add_argument(
|
|
"--odoo_version",
|
|
help="Select the odoo version.",
|
|
)
|
|
parser.add_argument(
|
|
"--erplibre_version",
|
|
help="Select the erplibre version.",
|
|
)
|
|
parser.add_argument(
|
|
"--erplibre_package",
|
|
help=(
|
|
"Select the erplibre package to configure environnement only for"
|
|
" this package."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--install",
|
|
action="store_true",
|
|
help="Install environnement.",
|
|
)
|
|
parser.add_argument(
|
|
"--switch",
|
|
action="store_true",
|
|
help="Will only update the system by switch environnement.",
|
|
)
|
|
parser.add_argument(
|
|
"--switch_update",
|
|
action="store_true",
|
|
help="Work with --switch, will do repo update",
|
|
)
|
|
parser.add_argument(
|
|
"--install_dev",
|
|
action="store_true",
|
|
help="Install developer environment.",
|
|
)
|
|
parser.add_argument(
|
|
"--partial_install",
|
|
action="store_true",
|
|
help="Preparation environment file, without installation. Docker need this",
|
|
)
|
|
parser.add_argument(
|
|
"--force_install",
|
|
action="store_true",
|
|
help="Will erase .venv.odooVersion and create symbolic link after installation.",
|
|
)
|
|
parser.add_argument(
|
|
"--force_repo",
|
|
action="store_true",
|
|
help="Will erase all repo before install it.",
|
|
)
|
|
parser.add_argument(
|
|
"--force",
|
|
action="store_true",
|
|
help="Force all, include force_install, force_repo.",
|
|
)
|
|
parser.add_argument(
|
|
"--with_extra",
|
|
action="store_true",
|
|
help=(
|
|
"Install or switch with extra modules (e.g. CybroOdoo)."
|
|
" State is saved in .erplibre-state.json."
|
|
),
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
if args.force:
|
|
args.force_install = True
|
|
args.force_repo = True
|
|
args.is_in_installation = args.install or args.install_dev
|
|
args.is_in_switch = args.switch
|
|
args.is_in_switch_force_update = args.switch and args.switch_update
|
|
if not (args.install or args.install_dev) and args.force_install:
|
|
args.force_install = False
|
|
|
|
return args
|
|
|
|
|
|
class Update:
|
|
def __init__(self):
|
|
self.execute_log = []
|
|
self.config = get_config()
|
|
self.data_version = None
|
|
self.odoo_version = None
|
|
self.python_version = None
|
|
self.mobile_active = False
|
|
self.detected_version_erplibre = None
|
|
self.new_version_erplibre = None
|
|
self.new_version_odoo = None
|
|
self.new_version_python = None
|
|
self.new_version_poetry = None
|
|
self.expected_odoo_name = None
|
|
self.expected_odoo_path = None
|
|
self.expected_addons_name = None
|
|
self.expected_venv_name = None
|
|
self.expected_manifest_name = None
|
|
self.expected_manifest_path = None
|
|
self.expected_pyproject_name = None
|
|
self.expected_pyproject_path = None
|
|
self.expected_poetry_lock_name = None
|
|
self.expected_poetry_lock_path = None
|
|
|
|
def check_version_data(self):
|
|
die(
|
|
not os.path.isfile(VERSION_DATA_FILE),
|
|
f"Missing {VERSION_DATA_FILE} path, are you sure you run this"
|
|
" script at root of the project?",
|
|
)
|
|
|
|
with open(VERSION_DATA_FILE) as txt:
|
|
self.data_version = json.load(txt)
|
|
|
|
if self.config.list:
|
|
for key, value in self.data_version.items():
|
|
print(f"{key}: {value}")
|
|
|
|
def detect_version(self):
|
|
# Detect actual version
|
|
if not os.path.exists(VERSION_PYTHON_FILE) or not os.path.exists(
|
|
VERSION_ODOO_FILE
|
|
):
|
|
_logger.info("New installation, cannot detect actual version.")
|
|
return False
|
|
with open(VERSION_PYTHON_FILE) as txt:
|
|
self.python_version = txt.read().strip()
|
|
with open(VERSION_ODOO_FILE) as txt:
|
|
self.odoo_version = txt.read().strip()
|
|
with open(VERSION_POETRY_FILE) as txt:
|
|
poetry_version = txt.read().strip()
|
|
|
|
# Detect mobile context
|
|
self.mobile_active = os.path.isdir(MOBILE_PATH)
|
|
|
|
# Show actual version
|
|
# « d'Odoo » nommé : le venv d'outillage tourne sur une AUTRE version,
|
|
# et un libellé nu laissait croire que le dépôt n'en a qu'une.
|
|
_logger.info(f"Python version (Odoo): {self.python_version}")
|
|
if os.path.exists(VERSION_PYTHON_ERPLIBRE_FILE):
|
|
with open(VERSION_PYTHON_ERPLIBRE_FILE) as txt:
|
|
_logger.info(
|
|
f"Python version (outillage): {txt.read().strip()}"
|
|
)
|
|
_logger.info(f"Odoo version: {self.odoo_version}")
|
|
_logger.info(f"Poetry version: {poetry_version}")
|
|
_logger.info(
|
|
f"Mobile context: {'active (' + MOBILE_PATH + ')' if self.mobile_active else 'inactive'}"
|
|
)
|
|
if _STATE_AVAILABLE:
|
|
extra = get_version_extra(self.odoo_version)
|
|
_logger.info(
|
|
f"Extra modules (CybroOdoo): {'active' if extra else 'inactive'}"
|
|
)
|
|
erplibre_version_to_search = ERPLIBRE_TEMPLATE_VERSION % (
|
|
self.odoo_version,
|
|
self.python_version,
|
|
)
|
|
|
|
# Detect key actual version
|
|
for key, value in self.data_version.items():
|
|
if (
|
|
value.get("odoo_version") == self.odoo_version
|
|
and value.get("python_version") == self.python_version
|
|
):
|
|
self.detected_version_erplibre = key
|
|
_logger.info(
|
|
f"Detected erplibre version {erplibre_version_to_search}"
|
|
)
|
|
break
|
|
if not self.detected_version_erplibre:
|
|
_logger.error(
|
|
f"The actual version '{erplibre_version_to_search}' is not"
|
|
f" configured into '{VERSION_DATA_FILE}'. Please update this"
|
|
" file before continue."
|
|
)
|
|
return False
|
|
|
|
if os.path.exists(INSTALLED_ODOO_VERSION_FILE):
|
|
with open(INSTALLED_ODOO_VERSION_FILE) as txt:
|
|
lst_version_installed = sorted(txt.read().splitlines())
|
|
str_installed_version = "Installed version: " + ", ".join(
|
|
lst_version_installed
|
|
)
|
|
_logger.info(str_installed_version)
|
|
return True
|
|
|
|
def validate_version(self):
|
|
dct_data_version = {}
|
|
if (
|
|
not self.config.python_version
|
|
and not self.config.poetry_version
|
|
and not self.config.odoo_version
|
|
and not self.config.erplibre_version
|
|
and not self.config.erplibre_package
|
|
):
|
|
dct_data_version = {}
|
|
if self.detected_version_erplibre:
|
|
dct_data_version = self.data_version.get(
|
|
self.detected_version_erplibre
|
|
)
|
|
if not dct_data_version:
|
|
# Take default version
|
|
default_data = [
|
|
key
|
|
for key, value in self.data_version.items()
|
|
if value.get("default")
|
|
]
|
|
if not default_data:
|
|
_logger.error(
|
|
"Cannot find default version into file"
|
|
f" {VERSION_DATA_FILE}"
|
|
)
|
|
sys.exit(1)
|
|
dct_data_version = self.data_version.get(default_data[0])
|
|
|
|
has_new_version = False
|
|
if self.config.erplibre_version:
|
|
data = self.data_version.get(self.config.erplibre_version)
|
|
if not data:
|
|
_logger.error(
|
|
"Missing data for erplibre_version"
|
|
f" {self.config.erplibre_version}"
|
|
)
|
|
sys.exit(1)
|
|
self.new_version_odoo = data.get("odoo_version")
|
|
self.new_version_python = data.get("python_version")
|
|
self.new_version_poetry = data.get("poetry_version")
|
|
has_new_version = True
|
|
if self.config.python_version:
|
|
self.new_version_python = self.config.python_version
|
|
has_new_version = True
|
|
if self.config.poetry_version:
|
|
self.new_version_poetry = self.config.poetry_version
|
|
has_new_version = True
|
|
if self.config.odoo_version:
|
|
self.new_version_odoo = self.config.odoo_version
|
|
has_new_version = True
|
|
if has_new_version:
|
|
self.new_version_erplibre = ERPLIBRE_TEMPLATE_VERSION % (
|
|
self.new_version_odoo,
|
|
self.new_version_python,
|
|
)
|
|
else:
|
|
_logger.info(
|
|
"No difference between detected version and new version:"
|
|
f" {self.detected_version_erplibre}"
|
|
)
|
|
if not dct_data_version:
|
|
_logger.error("Cannot find dct_data_version")
|
|
else:
|
|
self.new_version_erplibre = ERPLIBRE_TEMPLATE_VERSION % (
|
|
dct_data_version.get("odoo_version"),
|
|
dct_data_version.get("python_version"),
|
|
)
|
|
self.new_version_odoo = dct_data_version.get("odoo_version")
|
|
self.new_version_python = dct_data_version.get(
|
|
"python_version"
|
|
)
|
|
self.new_version_poetry = dct_data_version.get(
|
|
"poetry_version"
|
|
)
|
|
# self.new_version_erplibre = self.detected_version_erplibre
|
|
# self.new_version_odoo = self.odoo_version
|
|
# self.new_version_python = self.python_version
|
|
# TODO needs to detect actual poetry? No?
|
|
|
|
self.expected_venv_name = (
|
|
VENV_TEMPLATE_FILE % self.new_version_erplibre
|
|
)
|
|
self.expected_manifest_name = (
|
|
MANIFEST_TEMPLATE_FILE % self.new_version_odoo
|
|
)
|
|
self.expected_manifest_path = os.path.join(
|
|
".", "manifest", self.expected_manifest_name
|
|
)
|
|
self.expected_pyproject_name = (
|
|
PYPROJECT_TEMPLATE_FILE % self.new_version_erplibre
|
|
)
|
|
self.expected_pyproject_path = os.path.join(
|
|
".", "requirement", self.expected_pyproject_name
|
|
)
|
|
|
|
self.expected_poetry_lock_name = (
|
|
POETRY_LOCK_TEMPLATE_FILE % self.new_version_erplibre
|
|
)
|
|
self.expected_poetry_lock_path = os.path.join(
|
|
".", "requirement", self.expected_poetry_lock_name
|
|
)
|
|
self.expected_pip_requirement_name = (
|
|
PIP_REQUIREMENT_TEMPLATE_FILE % self.new_version_erplibre
|
|
)
|
|
self.expected_addons_name = (
|
|
ADDONS_TEMPLATE_FILE % self.new_version_odoo
|
|
)
|
|
self.expected_odoo_name = ODOO_TEMPLATE_FILE % self.new_version_odoo
|
|
self.expected_odoo_path = os.path.join(
|
|
".", self.expected_odoo_name, "odoo", "."
|
|
)
|
|
self.expected_pip_requirement_path = os.path.join(
|
|
".", "requirement", self.expected_pip_requirement_name
|
|
)
|
|
self.expected_pip_ignore_requirement_name = (
|
|
PIP_IGNORE_REQUIREMENT_TEMPLATE_FILE % self.new_version_erplibre
|
|
)
|
|
self.expected_pip_ignore_requirement_path = os.path.join(
|
|
".", "requirement", self.expected_pip_ignore_requirement_name
|
|
)
|
|
|
|
if self.config.erplibre_package:
|
|
_logger.warning("Not supported erplibre_package configuration")
|
|
|
|
def validate_environment(self):
|
|
status = True
|
|
venv_exist = os.path.exists(self.expected_venv_name)
|
|
if not venv_exist and not self.config.install_dev:
|
|
_logger.info("Relaunch this script with --install_dev argument.")
|
|
# Validate Odoo repo
|
|
# TODO need to remove this from architecture
|
|
# Validate Poetry and pip
|
|
status &= self.update_link_file(
|
|
"Poetry project toml",
|
|
PYPROJECT_FILE,
|
|
self.expected_pyproject_path,
|
|
do_delete_source=True,
|
|
)
|
|
status &= self.update_link_file(
|
|
"Poetry lock",
|
|
POETRY_LOCK_FILE,
|
|
self.expected_poetry_lock_path,
|
|
do_delete_source=True,
|
|
)
|
|
if not os.path.isdir(self.expected_addons_name):
|
|
status = False
|
|
return status
|
|
|
|
def update_environment(self):
|
|
status = True
|
|
if self.config.force_repo:
|
|
# TODO add script to check difference before erase all
|
|
os.system("./script/git/clean_repo_manifest.sh")
|
|
self.execute_log.append(
|
|
f"Clear all repo from manifest, everything is deleted"
|
|
)
|
|
|
|
# Always overwrite version
|
|
_logger.info(
|
|
f"Update local file, "
|
|
f"python version '{self.new_version_python}', "
|
|
f"erplibre version '{self.new_version_erplibre}', "
|
|
f"odoo version '{self.new_version_odoo}', "
|
|
f"poetry version '{self.new_version_poetry}'"
|
|
)
|
|
with open(VERSION_PYTHON_FILE, "w") as txt:
|
|
txt.write(self.new_version_python)
|
|
with open(VERSION_ERPLIBRE_FILE, "w") as txt:
|
|
txt.write(self.new_version_erplibre)
|
|
with open(VERSION_ODOO_FILE, "w") as txt:
|
|
txt.write(self.new_version_odoo)
|
|
with open(VERSION_POETRY_FILE, "w") as txt:
|
|
txt.write(self.new_version_poetry)
|
|
|
|
if self.config.is_in_installation or self.config.is_in_switch:
|
|
# TODO need to be force if installation path is all good, return True
|
|
if self.config.install_dev:
|
|
_logger.info("Installation.")
|
|
status = self.install_erplibre()
|
|
if _STATE_AVAILABLE and not status:
|
|
set_version_installed(
|
|
self.new_version_odoo,
|
|
extra=bool(self.config.with_extra),
|
|
python=self.new_version_python,
|
|
poetry=self.new_version_poetry,
|
|
)
|
|
elif (
|
|
self.config.is_in_switch
|
|
and self.config.is_in_switch_force_update
|
|
):
|
|
_logger.info("Switch")
|
|
self.execute_log.append(f"System update")
|
|
# Auto-detect extra from state, warn if no state recorded
|
|
with_extra = bool(self.config.with_extra)
|
|
if _STATE_AVAILABLE and not with_extra:
|
|
if not get_version_installed(self.new_version_odoo):
|
|
_logger.warning(
|
|
f"No installation state found for odoo"
|
|
f" {self.new_version_odoo}. Proceeding without"
|
|
f" extra modules (CybroOdoo). To install with"
|
|
f" extra: make"
|
|
f" install_odoo_{self.new_version_odoo.replace('.', '')}_with_extra"
|
|
)
|
|
else:
|
|
with_extra = get_version_extra(self.new_version_odoo)
|
|
manifest_script = (
|
|
"./script/manifest/update_manifest_local_dev.sh"
|
|
)
|
|
if with_extra:
|
|
manifest_script += " --with_extra"
|
|
status = os.system(manifest_script)
|
|
if _STATE_AVAILABLE and not status:
|
|
set_version_switched(self.new_version_odoo)
|
|
|
|
# To support multiple addons directory, remove TEMP
|
|
# for addons_path in os.listdir("."):
|
|
# if (
|
|
# addons_path.startswith("addons")
|
|
# and addons_path != addons_path_with_version
|
|
# and addons_path.endswith("TEMP")
|
|
# ):
|
|
# os.rename(addons_path, addons_path[:-4])
|
|
for addons_path in os.listdir("."):
|
|
if addons_path.startswith("addons"):
|
|
# In same time, force to create addons if not existing
|
|
addons_dir_path = os.path.join(addons_path, "addons")
|
|
if not os.path.isdir(addons_dir_path):
|
|
os.makedirs(addons_dir_path)
|
|
|
|
# Force create addons link
|
|
# if os.path.isdir(ADDONS_PATH):
|
|
# if os.path.islink(ADDONS_PATH):
|
|
# os.remove(ADDONS_PATH)
|
|
# else:
|
|
# os.rename(
|
|
# ADDONS_PATH,
|
|
# ADDONS_PATH
|
|
# + "_"
|
|
# + time.strftime("%Yy%mm%dd-%Hh%Mm%Ss"),
|
|
# )
|
|
# os.symlink(addons_path_with_version, ADDONS_PATH)
|
|
return status
|
|
|
|
def print_log(self):
|
|
if not self.execute_log:
|
|
_logger.info("Nothing to do")
|
|
return
|
|
_logger.info("List of execution log :")
|
|
for log_info in self.execute_log:
|
|
_logger.info("\t" + log_info)
|
|
|
|
def pycharm_update(self):
|
|
pycharm_is_installed = os.path.exists(".idea")
|
|
if not pycharm_is_installed or not self.execute_log:
|
|
return
|
|
os.system(
|
|
"./.venv.erplibre/bin/python ./script/ide/pycharm_configuration.py --init"
|
|
)
|
|
|
|
def install_erplibre(self):
|
|
self.execute_log.append(f"Dev installation")
|
|
status = os.system("./script/install/install_locally_dev.sh")
|
|
return status
|
|
|
|
def apply_extra_modules(self):
|
|
"""Add the extra modules (CybroOdoo) to the repo manifest, EVEN when
|
|
the base environment is already installed. Records the extra state so
|
|
git_merge_repo_manifest includes manifest/git_manifest_extra_odooXX.xml,
|
|
then re-generates the local manifest and re-syncs the repos (the Odoo
|
|
config is regenerated by the post-install step that follows).
|
|
|
|
Without this, « --with_extra » on an already-installed env was a no-op:
|
|
update_environment() (where the extra state is set) is skipped when the
|
|
env is valid, so get_version_extra() stayed False and CybroOdoo was
|
|
never cloned nor added to config.conf. Returns the os.system status."""
|
|
_logger.info("Applying extra modules (CybroOdoo) to the manifest...")
|
|
if _STATE_AVAILABLE:
|
|
set_version_installed(
|
|
self.new_version_odoo,
|
|
extra=True,
|
|
python=self.new_version_python,
|
|
poetry=self.new_version_poetry,
|
|
)
|
|
self.execute_log.append("Extra modules (CybroOdoo)")
|
|
return os.system("./script/manifest/update_manifest_local_dev.sh")
|
|
|
|
def add_extra_to_config_conf(self):
|
|
"""Append the extra addons (CybroOdoo, ...) to config.conf addons_path.
|
|
|
|
generate_config.sh rewrites config.conf from a fixed addons list that
|
|
does NOT include the extra modules, so this must run AFTER it (last
|
|
writer wins). The paths come from the source-of-truth extra manifest
|
|
(projects tagged with the "extra" group). Already-present paths are
|
|
skipped, so the call is idempotent."""
|
|
import xml.etree.ElementTree as ET
|
|
|
|
ver = self.new_version_odoo # e.g. "18.0"
|
|
manifest = os.path.join(
|
|
"manifest", f"git_manifest_extra_odoo{ver}.xml"
|
|
)
|
|
config_path = "config.conf"
|
|
if not os.path.isfile(manifest) or not os.path.isfile(config_path):
|
|
_logger.warning(
|
|
"Cannot add extra modules to config.conf:"
|
|
f" missing {manifest} or {config_path}."
|
|
)
|
|
return
|
|
home = os.getcwd()
|
|
prefix = f"odoo{ver}/addons/"
|
|
extra_paths = []
|
|
for project in ET.parse(manifest).getroot().findall("project"):
|
|
groups = project.get("groups", "").split(",")
|
|
path = project.get("path", "")
|
|
if "extra" in groups and path.startswith(prefix):
|
|
extra_paths.append(os.path.join(home, path))
|
|
if not extra_paths:
|
|
return
|
|
|
|
with open(config_path, "r", encoding="utf-8") as f:
|
|
lines = f.readlines()
|
|
changed = False
|
|
for i, line in enumerate(lines):
|
|
if not line.startswith("addons_path"):
|
|
continue
|
|
value = line.rstrip("\n").split("=", 1)[1] if "=" in line else ""
|
|
existing = [p.strip() for p in value.split(",") if p.strip()]
|
|
missing = [p for p in extra_paths if p not in existing]
|
|
if missing:
|
|
lines[i] = (
|
|
"addons_path = " + ",".join(existing + missing) + "\n"
|
|
)
|
|
changed = True
|
|
break
|
|
if changed:
|
|
with open(config_path, "w", encoding="utf-8") as f:
|
|
f.writelines(lines)
|
|
_logger.info(
|
|
"Added extra addons to config.conf: " + ", ".join(extra_paths)
|
|
)
|
|
else:
|
|
_logger.info("Extra addons already present in config.conf.")
|
|
|
|
def install_system(self):
|
|
self.execute_log.append(f"System installation")
|
|
status = os.system("./script/install/install_dev.sh")
|
|
return status
|
|
|
|
def update_link_file(
|
|
self,
|
|
component_name,
|
|
source_file,
|
|
target_file,
|
|
is_directory=False,
|
|
do_delete_source=False,
|
|
):
|
|
"""Call to create a symbolic link
|
|
0. check if file is symlink or file/directory
|
|
1.
|
|
Case 1 : origin file exist, need to be switched by a symlink and rename it;
|
|
Case 2 : no file, check if new version exist and symlink it;
|
|
Case 3 : source file is symlink, and wrong direction, erase it. If new version exist, symlink it.
|
|
Case 4: origin file exist, but it's a wrong symlink
|
|
Good case : source file is symlink, and good redirection.
|
|
"""
|
|
# TODO support case symlink is invalid
|
|
status = False
|
|
if not source_file or not target_file:
|
|
_logger.error(
|
|
f"{component_name}:Source file or target file is empty."
|
|
)
|
|
source_file_exist = os.path.exists(source_file)
|
|
source_file_is_symlink = os.path.islink(source_file)
|
|
target_file_exist = os.path.exists(target_file)
|
|
if not target_file_exist:
|
|
_logger.error(f"'{target_file}' not exist.")
|
|
do_symlink = False
|
|
# Case 4
|
|
if source_file_is_symlink and not source_file_exist:
|
|
_logger.error(
|
|
f"{component_name}:File '{source_file}' is a wrong symlink,"
|
|
" delete it."
|
|
)
|
|
os.system(f"rm {source_file}")
|
|
self.execute_log.append(
|
|
f"{component_name}:Delete file {source_file}"
|
|
)
|
|
source_file_is_symlink = False
|
|
|
|
# Case 1
|
|
if source_file_exist:
|
|
if not source_file_is_symlink:
|
|
# Check if source type
|
|
if is_directory:
|
|
source_file_is_directory = os.path.isdir(source_file)
|
|
if not source_file_is_directory:
|
|
_logger.error(
|
|
f"{component_name} - Source '{source_file}' is"
|
|
" expected to be a directory and it's not."
|
|
)
|
|
os.system(f"ls -lha {source_file}")
|
|
sys.exit(1)
|
|
# else:
|
|
# do_switch_origin_sim = True
|
|
else:
|
|
source_file_is_file = os.path.isfile(source_file)
|
|
if not source_file_is_file:
|
|
_logger.error(
|
|
f"{component_name} - Source '{source_file}' is"
|
|
" expected to be a file and it's not."
|
|
)
|
|
os.system(f"ls -lha {source_file}")
|
|
sys.exit(1)
|
|
else:
|
|
# Case 3
|
|
# Is symlink
|
|
lst_path_source = [
|
|
a for a in os.path.split(remove_dot_path(source_file)) if a
|
|
]
|
|
ref_symlink_source = os.readlink(source_file).strip("/")
|
|
lst_path_symlink_source = [
|
|
a
|
|
for a in os.path.split(remove_dot_path(ref_symlink_source))
|
|
if a
|
|
]
|
|
if (
|
|
len(lst_path_source) != len(lst_path_symlink_source)
|
|
and len(lst_path_symlink_source) == 1
|
|
):
|
|
ref_symlink_source_from_root = os.path.join(
|
|
".",
|
|
os.sep.join(lst_path_source[:-1]),
|
|
ref_symlink_source,
|
|
)
|
|
else:
|
|
ref_symlink_source_from_root = ref_symlink_source
|
|
do_delete_source = do_delete_source or os.path.exists(
|
|
target_file
|
|
)
|
|
if os.path.normpath(
|
|
ref_symlink_source_from_root
|
|
) == os.path.normpath(target_file):
|
|
_logger.info(
|
|
f"{component_name}:The system configuration is good."
|
|
)
|
|
status = True
|
|
elif do_delete_source:
|
|
_logger.warning(
|
|
f"{component_name}:The file '{source_file}' link"
|
|
f" '{ref_symlink_source_from_root}' will be delete and"
|
|
f" link to '{target_file}'."
|
|
)
|
|
# symlink, delete it if wrong
|
|
os.system(f"rm {source_file}")
|
|
self.execute_log.append(
|
|
f"{component_name}:Delete file {source_file}"
|
|
)
|
|
if target_file_exist:
|
|
do_symlink = True
|
|
else:
|
|
_logger.warning(
|
|
f"{component_name}:The actual link is"
|
|
f" '{ref_symlink_source_from_root}', but expect to be"
|
|
f" '{target_file}'."
|
|
)
|
|
|
|
elif target_file_exist:
|
|
# Case 2
|
|
do_symlink = True
|
|
if do_symlink:
|
|
lst_path_target = [
|
|
a for a in os.path.split(remove_dot_path(target_file)) if a
|
|
]
|
|
lst_path_source = [
|
|
a for a in os.path.split(remove_dot_path(source_file)) if a
|
|
]
|
|
if len(lst_path_source) == 1:
|
|
os.symlink(target_file, source_file)
|
|
msg = (
|
|
f"{component_name}:Create symbolic link {source_file} to"
|
|
f" {target_file}"
|
|
)
|
|
_logger.info(msg)
|
|
self.execute_log.append(msg)
|
|
else:
|
|
target_file_name = lst_path_target[-1]
|
|
source_file_name = lst_path_source[-1]
|
|
# TODO compare lst_path_source avec lst_path_target, si différent, faire le path
|
|
cd_path_target = (
|
|
"." + os.sep + os.sep.join(lst_path_target[:-1])
|
|
)
|
|
cmd_symlink_manifest = (
|
|
f"cd {cd_path_target};ln -s"
|
|
f" {target_file_name} {source_file_name};cd -"
|
|
)
|
|
os.system(cmd_symlink_manifest)
|
|
msg = (
|
|
f"{component_name}:Create symbolic link"
|
|
f" {source_file_name} to {target_file_name} from path"
|
|
f" {cd_path_target}"
|
|
)
|
|
_logger.info(msg)
|
|
self.execute_log.append(msg)
|
|
return status
|
|
|
|
|
|
def remove_dot_path(path):
|
|
"""
|
|
if path is ./path/2, will return path/2
|
|
"""
|
|
if path.startswith("./"):
|
|
return path[2:]
|
|
return path
|
|
|
|
|
|
def main():
|
|
update = Update()
|
|
_logger.info(f"Work on directory {os.getcwd()}")
|
|
_logger.info("Get data version")
|
|
update.check_version_data()
|
|
|
|
_logger.info("Detect version")
|
|
update.detect_version()
|
|
|
|
_logger.info("Validate version")
|
|
update.validate_version()
|
|
|
|
_logger.info("Validate environment")
|
|
# exit_code : propagé à sys.exit() -> une install qui ÉCHOUE renvoie un
|
|
# code non nul (avant, main() ne sortait jamais en erreur : le suivi
|
|
# d'installation affichait ✅ même quand poetry/pyenv plantaient).
|
|
# Rappel os.system : 0 = succès ; toute autre valeur = échec.
|
|
exit_code = 0
|
|
status = 0
|
|
if (
|
|
update.config.install_dev
|
|
or update.config.partial_install
|
|
or update.config.is_in_switch
|
|
):
|
|
status = update.validate_environment()
|
|
if update.config.install:
|
|
status = update.install_system()
|
|
if status not in (0, True, None):
|
|
exit_code = 1
|
|
if (
|
|
update.config.force_install
|
|
or update.config.install_dev
|
|
or update.config.partial_install
|
|
or update.config.is_in_switch
|
|
) and not status:
|
|
rc = update.update_environment()
|
|
if rc not in (0, True, None):
|
|
exit_code = 1
|
|
update.print_log()
|
|
|
|
# --with_extra : appliquer les modules extra (CybroOdoo) MÊME si l'env de
|
|
# base est déjà installé (sinon update_environment est sauté -> « Nothing
|
|
# to do » et CybroOdoo n'est jamais ajouté au repo ni à config.conf).
|
|
# Placé AVANT la régénération de config (post-étape) pour qu'elle le prenne.
|
|
if update.config.with_extra and exit_code == 0:
|
|
rc = update.apply_extra_modules()
|
|
if rc not in (0, True, None):
|
|
exit_code = 1
|
|
|
|
# Étapes post-install : uniquement si l'installation a RÉUSSI (sinon
|
|
# .venv.erplibre est absent -> cascade d'erreurs inutiles).
|
|
if update.config.install_dev and exit_code == 0:
|
|
# Update pycharm configuration
|
|
update.pycharm_update()
|
|
|
|
# Update OCB configuration
|
|
os.system(
|
|
"./.venv.erplibre/bin/python ./script/git/git_repo_update_group.py"
|
|
)
|
|
os.system("./script/generate_config.sh")
|
|
|
|
# config.conf : generate_config.sh (ci-dessus) réécrit le fichier à partir
|
|
# d'une liste d'addons figée SANS les modules extra ; on ajoute donc les
|
|
# chemins extra APRÈS coup (dernier writer). Idempotent, sans effet si
|
|
# --with_extra n'est pas demandé.
|
|
if update.config.with_extra and exit_code == 0:
|
|
update.add_extra_to_config_conf()
|
|
|
|
return exit_code
|
|
# TODO ignore this if installation fail
|
|
|
|
# TODO this cause an error at first execution, need to source ./.venv.erplibre/bin/activate and rerun
|
|
# subprocess.run(['source', './.venv.erplibre/bin/activate'], shell=True)
|
|
# subprocess.run(['make', 'config_gen_all'])
|
|
# status = os.system(f"make config_gen_all")
|
|
#
|
|
# if not status:
|
|
# print("Please run:")
|
|
# print("source ./.venv.erplibre/bin/activate")
|
|
# print("make config_gen_all")
|
|
|
|
|
|
def die(cond, message, code=1):
|
|
if cond:
|
|
print(message, file=sys.stderr)
|
|
sys.exit(code)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main() or 0)
|