diff --git a/.gitignore b/.gitignore index 7ac5d57dee63..226153a596a9 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,8 @@ env/ # Locale stats file src/backend/InvenTree/InvenTree/locale_stats.json src/backend/InvenTree/InvenTree/licenses.txt +src/backend/InvenTree/InvenTree/locale_stats.json +src/backend/InvenTree/InvenTree/constraint.txt # Logs src/backend/InvenTree/logs.json diff --git a/src/backend/InvenTree/InvenTree/config.py b/src/backend/InvenTree/InvenTree/config.py index 600e9e786455..fae7488bdca4 100644 --- a/src/backend/InvenTree/InvenTree/config.py +++ b/src/backend/InvenTree/InvenTree/config.py @@ -355,6 +355,34 @@ def get_backup_dir(create=True, error=True): return bd +def get_constraint_file(force_write: bool = False) -> Path: + """Returns the path to the constraints file. + + Args: + force_write (bool): If True, the constraints file will be created if it does not exist. + + Returns: + Path or str: The path to the constraints file. + """ + from .version import INVENTREE_SW_NXT_MAJOR, INVENTREE_SW_VERSION + + const_file = get_base_dir().joinpath('InvenTree', 'constraint.txt') + if force_write or (not const_file.exists()): + if not force_write: + logger.warning( + 'Constraint file does not exist - creating default file at %s', + const_file, + ) + ensure_dir(const_file.parent) + vers = INVENTREE_SW_VERSION.replace( + ' ', '' + ) # ensure that the uv resolver parses the condition correctly + const_file.write_text( + f'# InvenTree constraints file\ninventree-server~={vers},<{INVENTREE_SW_NXT_MAJOR}\n' + ) + return const_file + + def get_plugin_file() -> Path: """Returns the path of the InvenTree plugins specification file. diff --git a/src/backend/InvenTree/InvenTree/version.py b/src/backend/InvenTree/InvenTree/version.py index 52c9c6f59adc..d187d483e116 100644 --- a/src/backend/InvenTree/InvenTree/version.py +++ b/src/backend/InvenTree/InvenTree/version.py @@ -19,6 +19,7 @@ # InvenTree software version INVENTREE_SW_VERSION = '1.2.0 dev' +INVENTREE_SW_NXT_MAJOR = '2.0' # the next breaking major version logger = logging.getLogger('inventree') diff --git a/src/backend/InvenTree/plugin/installer.py b/src/backend/InvenTree/plugin/installer.py index 7a3b470672af..fd3693dcc4cb 100644 --- a/src/backend/InvenTree/plugin/installer.py +++ b/src/backend/InvenTree/plugin/installer.py @@ -12,6 +12,7 @@ import plugin.models import plugin.staticfiles +from InvenTree.config import get_constraint_file from InvenTree.exceptions import log_error logger = structlog.get_logger('inventree') @@ -134,7 +135,15 @@ def install_plugins_file(): logger.warning('Plugin file %s does not exist', str(pf)) return - cmd = ['install', '--disable-pip-version-check', '-U', '-r', str(pf)] + cmd = [ + 'install', + '--disable-pip-version-check', + '-U', + '-r', + str(pf), + '-c', + str(get_constraint_file()), + ] try: pip_command(*cmd) @@ -245,7 +254,13 @@ def install_plugin(url=None, packagename=None, user=None, version=None): logger.info('install_plugin: %s, %s', url, packagename) # build up the command - install_name = ['install', '-U', '--disable-pip-version-check'] + install_name = [ + 'install', + '-U', + '--disable-pip-version-check', + '-c', + str(get_constraint_file()), + ] full_pkg = '' @@ -291,6 +306,7 @@ def install_plugin(url=None, packagename=None, user=None, version=None): log_error('install_plugin', scope='plugins') if version := ret.get('version'): + # TODO do not pin version # Save plugin to plugins file update_plugins_file(packagename, full_package=full_pkg, version=version) diff --git a/tasks.py b/tasks.py index 8f7430b52a8e..0c00adf3952a 100644 --- a/tasks.py +++ b/tasks.py @@ -457,34 +457,52 @@ def check_file_existence(filename: Path, overwrite: bool = False): # Install tasks # region tasks +@task +@state_logger('TASK13') +def update_constraint(c): + """Update the constraints file for plugin installations.""" + from src.backend.InvenTree.InvenTree.config import get_constraint_file + + info('Generating constraint file for plugin installations...') + info('New constraint file written to: ', get_constraint_file(force_write=True)) + + @task(help={'uv': 'Use UV (experimental package manager)'}) @state_logger('TASK01') def plugins(c, uv=False): """Installs all plugins as specified in 'plugins.txt'.""" from src.backend.InvenTree.InvenTree.config import ( # type: ignore[import] + get_constraint_file, get_plugin_file, ) plugin_file = get_plugin_file() + constraint = get_constraint_file() - info(f"Installing plugin packages from '{plugin_file}'") + info( + f"Installing plugin packages from '{plugin_file}' with constraints '{constraint}'" + ) # Install the plugins if not uv: - run(c, f"pip3 install --disable-pip-version-check -U -r '{plugin_file}'") + run( + c, + f"pip3 install --disable-pip-version-check -U -r '{plugin_file}' -c '{constraint}'", + ) else: run(c, 'pip3 install --no-cache-dir --disable-pip-version-check uv') - run(c, f"uv pip install -r '{plugin_file}'") + run(c, f"uv pip install -r '{plugin_file}' -c '{constraint}'") # Collect plugin static files manage(c, 'collectplugins') @task( + pre=[update_constraint], help={ 'uv': 'Use UV package manager (experimental)', 'skip_plugins': 'Skip plugin installation', - } + }, ) @state_logger('TASK02') def install(c, uv=False, skip_plugins=False): @@ -845,6 +863,7 @@ def update( info('Updating InvenTree installation...') # Ensure required components are installed + update_constraint(c) install(c, uv=uv) if not skip_backup: