├── .gitignore ├── LICENSE.md ├── README.md ├── config.example.json ├── mods_manager.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | **/test/** 2 | **/.idea/** 3 | config.json 4 | /env 5 | .tool-versions -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Copyright (c) 2015 Octav "narc" Sandulescu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a tool for [Factorio](http://www.factorio.com/) headless server management. 2 | It provides mod management : List / Install / Update / Remove / Enable / Disable. 3 | 4 | This package is heavily inspired by [Factorio-mod-updater](https://github.com/astevens/factorio-mod-updater/blob/master/factorio-mod-updater) and [Factorio Updater](https://github.com/narc0tiq/factorio-updater) so big thanks to you for your inspiration ! 5 | 6 | # Table Of Content 7 | * [Coming from Factorio-Init ?](#coming-from-factorio-init-) 8 | * [Installation](#installation) 9 | * [Configuration](#configuration) 10 | * [Usage](#usage) 11 | * [A complex example](#a-complex-example) 12 | * [Username and token](#username-and-token) 13 | * [How to find the correct mod name](#how-to-find-the-correct-mod-name) 14 | * [A note on mod names containing spaces](#a-note-on-mod-names-containing-spaces) 15 | * [On the error "version `GLIBC_2.18' not found"](#on-the-error-version-glibc_218-not-found) 16 | * [A note on mod not installing / updating](#a-note-on-mod-not-installing--updating) 17 | * [About dependencies](#about-dependencies) 18 | * [Dependencies when installing](#dependencies-when-installing) 19 | * [Dependencies when removing](#dependencies-when-removing) 20 | * [Conflicts](#conflicts) 21 | * [License](#license) 22 | * [TODO](#todo) 23 | 24 | ## Coming from [Factorio-Init](https://github.com/Bisa/factorio-init) ? 25 | 26 | If you found this script by using [Factorio-Init](https://github.com/Bisa/factorio-init) and want a quick setup, just follow the installation step and... that's all ! [Factorio-Init](https://github.com/Bisa/factorio-init) will pass any needed configuration to [Factorio-mods-manager](https://github.com/Tantrisse/Factorio-mods-manager) (Path to factorio, Username, Token) ! 27 | 28 | You can still copy and edit the `config.json` file (see [Configuration](#configuration)) to customise the way the script works. 29 | 30 | Keep in mind that if you invoke this script via [Factorio-Init](https://github.com/Bisa/factorio-init) (`./factorio mod install XXXX`) these options are ignored from the `config.json` file as they come from [Factorio-Init](https://github.com/Bisa/factorio-init) : 31 | - factorio_path 32 | - username 33 | - token 34 | - alternative_glibc_directory 35 | - alternative_glibc_version 36 | 37 | ## Installation 38 | 39 | This script has been tested (only on Debian) with Python 2.7 and 3.9 using [Requests](http://requests.readthedocs.org/en/latest/) and [Packaging](https://pypi.org/project/packaging). 40 | 41 | 1. Clone this repository in any directory. Here, `/opt/factorio-mod-manager` as an example. 42 | ```shell script 43 | git clone git@github.com:Tantrisse/Factorio-mods-manager.git /opt/factorio-mod-manager 44 | ``` 45 | 46 | 2. Install the required dependency by running 47 | 48 | ```shell script 49 | pip install -r requirements.txt 50 | ``` 51 | 52 | If you can only use `easy_install` and not `pip`, try doing 53 | 54 | ```shell script 55 | easy_install `cat requirements.txt` 56 | ``` 57 | 58 | ## Configuration 59 | 60 | Some constant parameters can be put in a config file. The mandatory options which don't have a default values are `factorio_path`, `username` and `token`, these must be set via command line parameters or the config file. 61 | 62 | All other options have default value 63 | 64 | These options are 65 | 66 | | Option | Default | Definition | 67 | |----------------------------------:|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 68 | | **factorio_path** | none | The path to the root folder of your Factorio installation. | 69 | | **username** | none | Your username, see [Username and token](#username-and-token). | 70 | | **token** | none | Your token, see [Username and token](#username-and-token). | 71 | | **verbose** | false | Enable verbose (debug messages) mode. | 72 | | **should_downgrade** | false | If true, the script will install older version if no compatible version is found for the current Factorio version (see: [this note on mods not updating](#a-note-on-mod-not-installing--updating)). | 73 | | **install_required_dependencies** | true | If true, all required dependencies (and any required child dependencies) will be installed. | 74 | | **install_optional_dependencies** | false | If true, all optional dependencies will be installed. Note : optional dependencies of required/optional dependencies are never installed automatically. | 75 | | **remove_required_dependencies** | true | If true, all required dependencies (and any required child dependencies) will be removed when a parent is removed. | 76 | | **remove_optional_dependencies** | false | If true, all optional dependencies will be removed when a parent is removed. Note : optional dependencies of required/optional dependencies are never removed automatically. | 77 | | **ignore_conflicts_dependencies** | false | If true, any conflict between mods are ignored and mods are installed anyway. | 78 | | **should_reload** | false | If true, the script will try to reload Factorio via systemctl and the service_name parameter. | 79 | | **service_name** | none | If Factorio is started via a service and you want to restart it automatically. | 80 | | **alternative_glibc_directory** | false | Absolute path to the side by side GLIBC root, used for systems using older glibc versions (RHEL CentOS and others...) | 81 | | **alternative_glibc_version** | false | Version of alt GLIBC (2.18 is the minimum required for factorio) | 82 | 83 | An example file can be found in this repo, just copy `config.example.json` to `config.json` and edit values inside. 84 | 85 | Keep in mind that any corresponding command line argument will **override** these in config file. 86 | 87 | ## Usage 88 | 89 | There is too many possibilities to cover them all here ! 90 | 91 | But in short, you can install, uninstall, enable or disable mods. 92 | All of this with an automatic management of dependencies 93 | 94 | You can get a list of all commands and flags by simply going in the folder where you cloned this repo, and run 95 | 96 | ```shell script 97 | python mods_manager.py --help 98 | ``` 99 | 100 | -------- 101 | 102 | ### A complex example 103 | 104 | Here we want to : 105 | 106 | * Install "bobvehicleequipment" 107 | * Enable "bobplates" and "bobgreenhouse" (assuming they are already installed) 108 | * Disable "IndustrialRevolution" because of incompatibility issues with Bob's mods 109 | * Finally, we update all mods 110 | 111 | All this can be done in one command : 112 | ```shell script 113 | $ python mods_manager.py -p /app/projects/factorio/factorio -u YOUR_USER -t YOUR_TOKEN -i bobvehicleequipment -E bobplates -E bobgreenhouse -D IndustrialRevolution -U 114 | Enabling mod(s) ['bobplates', 'bobgreenhouse'] 115 | 116 | Disabling mod(s) ['IndustrialRevolution'] 117 | 118 | 119 | Installing dependency "boblibrary" version >= "1.1.0" for "bobvehicleequipment" 120 | [==================================================] 121 | Installed mod boblibrary version 1.1.2 for Factorio version 1.1 122 | [==================================================] 123 | Installed mod bobvehicleequipment version 1.1.2 for Factorio version 1.1 124 | 125 | The mod configuration changed and Factorio need to be restarted in order to apply the changes. 126 | Automatic reload has been disabled, please restart Factorio by yourself. 127 | Finished ! 128 | ``` 129 | 130 | ## Username and token 131 | 132 | The keen-eyed will have noticed the options for `--user` and `--token`. These 133 | allow you to supply a username and token normally used by the Factorio (like the in-game updater and authenticated multiplayer). Having them will 134 | allow you to download the mods from the [Factorio API](https://mods.factorio.com/api/mods). 135 | 136 | First, how to get them: 137 | * Go to [the Factorio website](https://www.factorio.com/login) and login to your account. 138 | * Click your username in top right to go to your profile. 139 | 140 | ## How to find the correct mod name 141 | 142 | In order to use this script, you have to find the correct mod name, not the "friendly" one. 143 | You can do it directly from [mod portal](https://mods.factorio.com/) ! 144 | 145 | Once you find an interesting mod, for example `Bob's Metals, Chemicals and Intermediates`, 146 | open the mod portal page, here https://mods.factorio.com/mod/bobplates 147 | 148 | The correct mod name to use is the last part of the URL after `/mod/` : `bobplates`. 149 | 150 | ### A note on mod names containing spaces 151 | 152 | If the mod you are looking for contains spaces in its name, like [Flow Control](https://mods.factorio.com/mod/Flow%20Control), 153 | don't forget to either : 154 | - [url encode](https://www.urlencoder.io/) the mode name, `Flow Control` becoming `Flow%20Control` 155 | - or add double quotes before and after the mod name to pass it as a single string 156 | - `python mods_manager.py -i "Flow Control"` 157 | - You can do it too if you use [Factorio-Init](https://github.com/Bisa/factorio-init) `factorio mod install "Flow Control"` 158 | 159 | ## On the error "version `GLIBC_2.18' not found" 160 | 161 | If you encounter an error about **GLIBC 2.18** not found, you can install it using [this thread on the factorio forum by **millisa**](https://forums.factorio.com/viewtopic.php?t=54654#p324493). 162 | 163 | When following the aforementioned guide, if you stumble across the error `These critical programs are missing or too old: make` when doing `../configure --prefix='/opt/glibc-2.18'` and your make version is up-to-date, just run this command and try again : 164 | ``` 165 | sed "s/3\.\[89\]/3\.\[89\]\* | 4/" -i ../configure 166 | ``` 167 | You should be able to finish the installation of GLIBC. 168 | 169 | After that, you need to add to the `config.json` file of `Factorio-mod-manager` these 2 key : `alternative_glibc_directory` and `alternative_glibc_version`. 170 | 171 | You can use the command line parameters `--alternative-glibc-directory` and `--alternative-glibc-version` instead of the `config.json` file. 172 | 173 | See [the part about configuration](#configuration) to know what value to pass. 174 | 175 | ## A note on mod not installing / updating 176 | 177 | When Factorio is updated to a newer version, it sometimes doesn't break / change anything for some mods. 178 | 179 | This creates a situation where (for example) there is no version listed of FNEI for Factorio 1.0.0 180 | ([api response](https://mods.factorio.com/api/mods/FNEI)) because the latest mod version for Factorio 0.18 works fine. 181 | 182 | The only way to install this mod when using Factorio 1.0.0 is by using the `--downgrad` flag. 183 | The script will now install / update the mod using the latest release available for Factorio < 1.0.0 (here Factorio 0.18). 184 | 185 | Beware that it don't check if the mod is compatible and should only be used if you're sure that all your mods 186 | are compatible with your Factorio version. 187 | 188 | ## About dependencies 189 | 190 | ### Dependencies when installing 191 | 192 | By default, the script will install any **required** dependencies. If a dependency has a required dependency, the script will install it as long as there is required dependencies. 193 | 194 | This behavior can be disabled (however not recommended) by passing the `-nrd` or `--no-required-dependencies` flag. 195 | 196 | Optional dependencies are not installed by default. It can be done by passing the `-iod` or `--install-optional-dependencies` flag. 197 | Note that only optional dependencies of mod you are currently installing are installed. The optional dependencies of dependencies are ignored. They should be installed on their own. 198 | 199 | ### Dependencies when removing 200 | 201 | When removing a mod, any **required** dependencies by this mod and their children will be deleted. 202 | 203 | This behavior can be disabled by passing the `-nrrd` or `--no-remove-required-dependencies` flag. 204 | 205 | Optional dependencies are not removed by default. It can be done by passing the `-rod` or `--remove-optional-dependencies` flag. 206 | Note that only optional dependencies of mod you are currently removing are removed. 207 | 208 | ### Conflicts 209 | 210 | The script will check for conflict between the mods already installed and the mod you are trying to install. 211 | 212 | If a conflict is found, the installation stop. 213 | 214 | To install anyway, you may use the flag `-icd` or `--ignore-conflicts-dependencies` to bypass this restriction (however really not recommended). 215 | 216 | ## License 217 | 218 | The source of **Factorio Mod Manager** is Copyright 2019 Tristan "Tantrisse" 219 | Chanove. It is licensed under the [MIT license][mit], available in this 220 | package in the file [LICENSE.md](LICENSE.md). 221 | 222 | [mit]: http://opensource.org/licenses/mit-license.html 223 | 224 | 225 | ## TODO 226 | - Add crontab example 227 | - Interactive mod 228 | - ~~Handle dependencies~~ (done, should update do it too ? Probably) 229 | - ~~Handle conflicts~~ (kinda done) 230 | - ~~Support multiple instances of Factorio~~ (will not do) 231 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "__comment" : "Fields prefixed by __comment are just... comments and don't carry any values, you can safely ignore them", 3 | 4 | "__comment_factorio_path": "Path to Factorio root folder. Example : /opt/factorio", 5 | "factorio_path": "", 6 | 7 | "__comment_username": "Your Username here. See README -> Username and token", 8 | "username": "", 9 | 10 | "__comment_token": "Your Token here. See README -> Username and token", 11 | "token": "", 12 | 13 | "__comment_verbose": "Enable verbose (debug messages) mode. Useful in case of bug or weird behavior.", 14 | "verbose": false, 15 | 16 | "__comment_should_downgrade": "Can be true or false. If true the script will install older version of mods if no compatible version is found for the current Factorio version (see README).", 17 | "should_downgrade": false, 18 | 19 | "__comment_install_required_dependencies": "Can be true or false. If true, all required dependencies (and any required child dependencies) will be installed.", 20 | "install_required_dependencies": true, 21 | 22 | "__comment_install_optional_dependencies": "Can be true or false. If true, all optional dependencies will be installed. Note : optional dependencies of required/optional dependencies are never installed automatically.", 23 | "install_optional_dependencies": false, 24 | 25 | "__comment_remove_required_dependencies": "Can be true or false. If true, all required dependencies (and any required child dependencies) will be removed when a parent is removed.", 26 | "remove_required_dependencies": true, 27 | 28 | "__comment_remove_optional_dependencies": "Can be true or false. If true, all optional dependencies will be removed when a parent is removed. Note : optional dependencies of required/optional dependencies are never removed automatically.", 29 | "remove_optional_dependencies": false, 30 | 31 | "__comment_ignore_conflicts_dependencies": "Can be true or false. If true, any conflict between mods are ignored and mods are installed anyway.", 32 | "ignore_conflicts_dependencies": false, 33 | 34 | "__comment_should_reload": "Can be true or false. If true, the script will try to reload Factorio via `systemctl restart` and the service_name parameter.", 35 | "should_reload": false, 36 | 37 | "__comment_service_name": "The name of the service used to start Factorio by systemctl.", 38 | "service_name": "", 39 | 40 | "__comment_alternative_glibc_directory": "Can be false if we should use the default one or the path to folder where glibc is installed. Eg: /opt/glibc-2.18 (see README -> On the error version `GLIBC_2.18' not found)", 41 | "alternative_glibc_directory": false, 42 | 43 | "__comment_alternative_glibc_version": "Ignored if alternative_glibc_directory is false. Otherwise the version of glibc in the 'alternative_glibc_directory'. Eg: 2.18 (see README -> On the error version `GLIBC_2.18' not found)", 44 | "alternative_glibc_version": false 45 | } 46 | -------------------------------------------------------------------------------- /mods_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | import os 5 | import requests 6 | import sys 7 | import json 8 | import hashlib 9 | import argparse 10 | import subprocess 11 | import re 12 | import copy 13 | from datetime import datetime 14 | from packaging.version import parse 15 | 16 | 17 | __location__ = os.path.dirname(os.path.realpath(__file__)) 18 | 19 | 20 | # Fix for python 2 FileNotFoundError 21 | try: 22 | FileNotFoundError 23 | except NameError: 24 | # noinspection PyShadowingBuiltins 25 | FileNotFoundError = IOError 26 | 27 | # Global parameters with default values 28 | glob = { 29 | 'verbose': False, 30 | 'dry_run': False, 31 | 'factorio_path': None, 32 | 'factorio_version': None, 33 | 'mods_folder_path': None, 34 | 'mods_list_path': None, 35 | 'username': None, 36 | 'token': None, 37 | 'should_reload': False, 38 | 'service_name': None, 39 | 'has_to_reload': None, 40 | 'should_downgrade': False, 41 | 'install_required_dependencies': True, 42 | 'install_optional_dependencies': False, 43 | 'remove_required_dependencies': True, 44 | 'remove_optional_dependencies': False, 45 | 'ignore_conflicts_dependencies': False, 46 | 'alternative_glibc_directory': False, 47 | 'alternative_glibc_version': False 48 | } 49 | 50 | 51 | # Global, utility functions 52 | def get_file_sha1(file_name): 53 | blocksize = 65536 54 | hasher = hashlib.sha1() 55 | with open(file_name, 'rb') as afile: 56 | buf = afile.read(blocksize) 57 | while len(buf) > 0: 58 | hasher.update(buf) 59 | buf = afile.read(blocksize) 60 | return hasher.hexdigest() 61 | 62 | 63 | parser = argparse.ArgumentParser(description="Install / Update / Remove mods for Factorio", formatter_class=argparse.RawTextHelpFormatter) 64 | 65 | group = parser.add_argument_group('Behavior') 66 | group.add_argument('-v', '--verbose', action='store_true', dest='verbose', 67 | help="Print URLs and stuff as they happen.") 68 | 69 | group.add_argument('-d', '--dry-run', action='store_true', dest='dry_run', 70 | help="Don't download files, just state which mods updates would be downloaded.") 71 | 72 | group.add_argument('--downgrade', action='store_true', dest='should_downgrade', 73 | help="If no compatible version is found, install / update the last mod version for precedent Factorio version.\n" 74 | "(ex: If mod has no Factorio 1.0.0 version, it will install the latest mod version for Factorio 0.18)") 75 | 76 | group = parser.add_argument_group('Local configuration (override config.json)') 77 | group.add_argument('-p', '--path-to-factorio', dest='factorio_path', 78 | help="Path to your Factorio folder.") 79 | 80 | group.add_argument('-u', '--user', dest='username', 81 | help="Your Factorio username, from player-data.json.") 82 | group.add_argument('-t', '--token', dest='token', 83 | help="Your Factorio token, from player-data.json.") 84 | 85 | group = parser.add_argument_group('Mod listing') 86 | group.add_argument('-l', '--list', action='store_true', dest='list_mods', 87 | help="List installed mods and return. Ignore other switches.") 88 | 89 | group = parser.add_argument_group('Mod installation') 90 | group.add_argument('-i', '--install', dest='mod_name_to_install', 91 | help="Install the given mod. See README to easily find the correct mod name.") 92 | 93 | group = parser.add_argument_group('Mod update') 94 | group.add_argument('-U', '--update', action='store_true', dest='should_update', 95 | help="Enable the update process. By default, all mods are updated. See -e/--update-enabled-only.") 96 | 97 | group.add_argument('-e', '--update-enabled-only', action='store_true', dest='enabled_only', 98 | help="Will only updates mods 'enabled' in 'mod-list.json'.") 99 | 100 | group = parser.add_argument_group('Mod removal') 101 | group.add_argument('-r', '--remove', dest='remove_mod_name', 102 | help="Remove specified mod.") 103 | 104 | group = parser.add_argument_group('Mod enabling / disabling') 105 | group.add_argument('-E', '--enable', dest='enable_mods_name', action='append', 106 | help="A mod name to enable. Repeat the flag for each mod you want to enable.") 107 | 108 | group.add_argument('-D', '--disable', dest='disable_mods_name', action='append', 109 | help="A mod name to disable. Repeat the flag for each mod you want to disable.") 110 | 111 | group = parser.add_argument_group('Service reloading (override config.json)') 112 | group.add_argument('--reload', action='store_true', dest='should_reload', 113 | help="Enable the restarting of Factorio if any mods are installed / updated. If set, service-name must be set.") 114 | 115 | group.add_argument('-s', '--service-name', dest='service_name', 116 | help="The service name used to launch Factorio. Do not pass anything if not the case (prevent reloading).") 117 | 118 | group = parser.add_argument_group('Dependencies management (override config.json)') 119 | group.add_argument('-nrd', '--no-required-dependencies', action='store_true', dest='disable_required_dependencies', 120 | help="Disable the auto-installation of REQUIRED dependencies.") 121 | 122 | group.add_argument('-nrrd', '--no-remove-required-dependencies', action='store_false', dest='remove_required_dependencies', 123 | help="Enable the removal of all the REQUIRED dependencies of the mod asked to be removed.") 124 | 125 | group.add_argument('-iod', '--install-optional-dependencies', action='store_true', dest='install_optional_dependencies', 126 | help="Enable the auto-installation of OPTIONAL dependencies.") 127 | 128 | group.add_argument('-rod', '--remove-optional-dependencies', action='store_true', dest='remove_optional_dependencies', 129 | help="Enable the removal of all the OPTIONAL dependencies of the mod asked to be removed.") 130 | 131 | group.add_argument('-icd', '--ignore-conflicts-dependencies', action='store_true', dest='ignore_conflicts_dependencies', 132 | help="Ignore any conflicts between mods.") 133 | 134 | group = parser.add_argument_group('Alternative GLIBC options (override config.json)') 135 | group.add_argument('--alternative-glibc-directory', dest='alt_glibc_dir', 136 | help="Path to the root directory of the alternative GLIBC library.") 137 | 138 | group.add_argument('--alternative-glibc-version', dest='alt_glibc_version', 139 | help="Version of the alternative GLIBC library.") 140 | 141 | group = parser.add_argument_group('Self Updating') 142 | group.add_argument('--update-mod-manager', action='store_true', dest='update_mod_manager', 143 | help="Update Factorio-mod-manager. Require GIT. Program will exit after, this flag should be used alone.") 144 | 145 | 146 | def find_version(): 147 | binary_path = os.path.join(glob['factorio_path'], 'bin/x64/factorio') 148 | 149 | cmd = [] 150 | if glob['alternative_glibc_directory'] is not False and glob['alternative_glibc_version'] is not False: 151 | cmd.extend(( 152 | "%s/lib/ld-%s.so" % (glob['alternative_glibc_directory'], glob['alternative_glibc_version']), 153 | "--library-path", 154 | "%s/lib" % glob['alternative_glibc_directory'], 155 | binary_path, 156 | "--executable-path" 157 | )) 158 | 159 | cmd.extend((binary_path, '--version')) 160 | version_output = subprocess.check_output(cmd, universal_newlines=True) 161 | # version_output = subprocess.check_output([binary_path, "--version"], universal_newlines=True) 162 | # We only capture the MAIN and MAJOR version because from a mod pov the minor version should never be specified 163 | # see : https://wiki.factorio.com/Tutorial:Mod_structure#info.json -> "factorio_version" 164 | # "Adding a minor version, e.g. "0.18.27" will make the mod portal reject the mod and the game act weirdly" 165 | source_version = re.search("Version: (\d+\.\d+)\.\d+ \(build \d+", version_output) 166 | if source_version: 167 | main_version = parse(source_version.group(1)) 168 | debug("Auto-detected Factorio version %s from binary." % main_version) 169 | return main_version 170 | 171 | 172 | glob_mod_list = [] 173 | 174 | 175 | def read_mods_list(remove_base=True): 176 | debug('Parsing "mod-list.json"...') 177 | global glob_mod_list 178 | 179 | if not glob_mod_list: 180 | try: 181 | with open(glob['mods_list_path'], 'r') as fd: 182 | json_decoded = json.load(fd) 183 | if 'mods' not in json_decoded: 184 | print('Error while reading the "mod-list.json" file in %s, there is no mods in it (no "mods" key) !' % glob['mods_list_path']) 185 | exit(1) 186 | glob_mod_list = json_decoded['mods'] 187 | except json.JSONDecodeError: 188 | print('Error while reading the "mod-list.json" file in %s, it cannot be parsed to Json !' % glob['mods_list_path']) 189 | exit(1) 190 | 191 | # Remove the 'base' mod 192 | installed_mods_list = copy.deepcopy(glob_mod_list) 193 | if remove_base: 194 | installed_mods_list[:] = [d for d in glob_mod_list if d.get('name') != 'base' and d.get('name') != 'elevated-rails' and d.get('name') != 'quality' and d.get('name') != 'space-age'] 195 | 196 | return installed_mods_list 197 | 198 | 199 | def add_to_glob_mod_list(new_mod): 200 | global glob_mod_list 201 | read_mods_list(False) 202 | 203 | mod_found = False 204 | for mod in glob_mod_list: 205 | if mod['name'] == new_mod['name']: 206 | mod['enabled'] = new_mod['enabled'] 207 | mod_found = True 208 | break 209 | 210 | if not mod_found: 211 | glob_mod_list.append(new_mod) 212 | 213 | 214 | def remove_to_glob_mod_list(mod_to_remove): 215 | global glob_mod_list 216 | read_mods_list(False) 217 | 218 | glob_mod_list[:] = [d for d in glob_mod_list if d.get('name') != mod_to_remove['name']] 219 | 220 | 221 | def write_mods_list(): 222 | debug('Writing to mod-list.json') 223 | global glob_mod_list 224 | mods_list_json = { 225 | "mods": glob_mod_list 226 | } 227 | if glob['dry_run']: 228 | print('Dry-running, would have writen this mods list : %s' % json.dumps(mods_list_json, indent=2)) 229 | return 230 | 231 | with open(glob['mods_list_path'], 'w') as fd: 232 | json.dump(mods_list_json, fd, indent=2) 233 | 234 | 235 | def remove_file(file_path): 236 | if os.path.isfile(file_path): 237 | if glob['dry_run']: 238 | print('Dry-running, would have deleted this file if it exists : %s' % file_path) 239 | return 240 | 241 | os.remove(file_path) 242 | 243 | 244 | def display_mods_list(mods_list): 245 | if len(mods_list) == 0: 246 | print('No mods are installed') 247 | return 248 | 249 | print('Currently installed mods :') 250 | for mod in mods_list: 251 | print(""" Mod name : %s 252 | Enabled : %s 253 | """ % (mod['name'], mod['enabled'])) 254 | 255 | 256 | def get_mod_infos(mod, min_mod_version='latest'): 257 | debug('Getting mod "%s" infos...' % (mod['name'])) 258 | request_url = 'https://mods.factorio.com/api/mods/' + mod['name'] + '/full' 259 | 260 | r = requests.get(request_url) 261 | if r.status_code != 200: 262 | print('Error getting mod "' + mod['name'] + '" infos. Ignoring this mod, please, check your "mod-list.json" file.') 263 | return False 264 | 265 | json_result = r.json() 266 | 267 | if 'releases' not in json_result or len(json_result['releases']) == 0: 268 | debug('Mod "%s" does not seems to have any release ! Skipping...' % (mod['name'])) 269 | return False 270 | 271 | sorted_releases = sorted(json_result['releases'], key=lambda i: datetime.strptime(i['released_at'], '%Y-%m-%dT%H:%M:%S.%fZ'), reverse=True) 272 | 273 | if min_mod_version == 'latest': 274 | if glob['should_downgrade'] is True: 275 | filtered_releases = [release for release in sorted_releases if parse(release['info_json']['factorio_version']) <= glob['factorio_version']] 276 | else: 277 | filtered_releases = [release for release in sorted_releases if parse(release['info_json']['factorio_version']) == glob['factorio_version']] 278 | 279 | else: 280 | filtered_releases = [release for release in sorted_releases if parse(release['version']) >= parse(min_mod_version)] 281 | 282 | if len(filtered_releases) == 0: 283 | print('Asked for mod "%s" at least version "%s" but no result found ! Skipping...' % ( 284 | mod['name'], 285 | min_mod_version 286 | )) 287 | return False 288 | 289 | mods_infos = { 290 | 'name': mod['name'], 291 | 'enabled': mod['enabled'], 292 | 'releases': sorted_releases, 293 | 'same_version_releases': filtered_releases 294 | } 295 | 296 | return mods_infos 297 | 298 | 299 | # Dependencies rules : 300 | # "no prefix" = required (must be installed) 301 | # "~" = required but does not affect load order (must be installed) 302 | # "?" = optional (can be installed) 303 | # "(?)" = hidden optional, used to change load order (should not be installed) 304 | # "!" = conflict (must NOT be installed) 305 | # "(!)" = conflict, should not exist, but I swear I saw it one time (must NOT be installed) 306 | def parse_dependencies(dependencies): 307 | filtered_dependencies = {"required": [], "optional": [], "conflict": []} 308 | 309 | for mod in dependencies: 310 | # We clean the mod name 311 | mod = "".join(mod.split()) 312 | # Split the version comparator. TODO : capture the comparator for later use 313 | mod = re.split('<|<=|=|>=|>', mod) 314 | 315 | if len(mod) == 1: 316 | mod.append('latest') 317 | 318 | # Skip "base", "!", "(!)", "?", "(?)" 319 | if mod[0].find('base') == -1 and not mod[0].startswith(('!', '(!)', '?', '(?)')): 320 | # TODO future : Split the name and version requirement 321 | if mod[0].startswith('~'): 322 | # Remove the first char : "~" 323 | mod[0] = mod[0][1:] 324 | filtered_dependencies['required'].append(mod) 325 | 326 | # we ignore dependencies starting with "(?)" as they are hidden optional 327 | # listed only for load order 328 | elif mod[0].startswith('?'): 329 | # Remove the first char : "?" 330 | mod[0] = mod[0][1:] 331 | # Split the name and version requirement 332 | filtered_dependencies['optional'].append(mod) 333 | 334 | # the case "(!)" should not exists but hey, we saw weird things from the API... 335 | elif mod[0].startswith('!') or mod[0].startswith('(!)'): 336 | # Remove the first char "!" or "(!)" 337 | mod[0] = mod[0][1:] if mod[0].startswith('!') else mod[0][3:] 338 | 339 | filtered_dependencies['conflict'].append(mod) 340 | 341 | return filtered_dependencies 342 | 343 | 344 | def mod_has_conflicts(conflict_list): 345 | # For each mod already installed 346 | for mod in read_mods_list(): 347 | # We check if this mod is not in the conflict list of the 348 | # mod we currently try to install 349 | if mod['name'] in conflict_list: 350 | return mod['name'] 351 | 352 | return False 353 | 354 | 355 | def check_file_and_sha(file_path, sha1): 356 | # We assume that a file with the same name and SHA1 is up-to-date 357 | if os.path.exists(file_path) and sha1 == get_file_sha1(file_path): 358 | print('A file already exists at the path "%s" and is identical (same SHA1), skipping...' % file_path) 359 | return True 360 | 361 | return False 362 | 363 | 364 | def update_mods(enabled_only): 365 | debug('Starting mods update...') 366 | 367 | mods_list = read_mods_list() 368 | 369 | for mod in mods_list: 370 | mod_infos = get_mod_infos(mod) 371 | 372 | if enabled_only and mod_infos['enabled'] is False: 373 | debug('Mod %s is disable and --update-enabled-only has been used. Skipping...' % (mod_infos['name'])) 374 | continue 375 | 376 | if len(mod_infos['same_version_releases']) == 0: 377 | print('No matching version found for the mod "%s". Skipping...' % (mod['name'])) 378 | continue 379 | 380 | delete_list = [release for release in mod_infos['releases'] if release['file_name'] not in [mod_infos['same_version_releases'][0]['file_name']]] 381 | for release in delete_list: 382 | file_path = os.path.join(glob['mods_folder_path'], release['file_name']) 383 | debug('Removing old release file : %s' % file_path) 384 | remove_file(file_path) 385 | 386 | file_path = os.path.join(glob['mods_folder_path'], mod_infos['same_version_releases'][0]['file_name']) 387 | if check_file_and_sha(file_path, mod_infos['same_version_releases'][0]['sha1']): 388 | continue 389 | 390 | debug('Downloading mod %s' % (mod_infos['name'])) 391 | download_mod(file_path, mod_infos['same_version_releases'][0]['download_url']) 392 | 393 | # Save globally that a reload of Factorio is needed in the end. 394 | glob['has_to_reload'] = True 395 | 396 | 397 | glob_install_mod_seen = {} 398 | 399 | 400 | def install_mod(mod_name, min_mod_version='latest', install_optional_dependencies=True): 401 | debug('Installing mod %s' % mod_name) 402 | global glob_install_mod_seen 403 | 404 | if mod_name in glob_install_mod_seen: 405 | print('Mod "%s" already seen, skipping...' % mod_name) 406 | return 407 | 408 | glob_install_mod_seen[mod_name] = True 409 | 410 | mod = { 411 | 'name': mod_name, 412 | 'enabled': True 413 | } 414 | mod_infos = get_mod_infos(mod, min_mod_version) 415 | if not mod_infos: 416 | debug('Mod "%s" not found ! Skipping installation.' % mod_name) 417 | return 418 | 419 | if len(mod_infos['same_version_releases']) == 0: 420 | print('No matching version found for the mod "%s". No mod has been installed !' % (mod['name'])) 421 | return 422 | 423 | # Filter the one release we'll use 424 | target_release = mod_infos['same_version_releases'][0] 425 | 426 | # Check for dependencies if needed 427 | dependencies = parse_dependencies(target_release['info_json']['dependencies']) 428 | # Check for conflicts 429 | conflict = mod_has_conflicts(dependencies['conflict']) 430 | if conflict is not False: 431 | print('Mod "%s" has a conflict with the mod "%s" already installed' % (mod_name, conflict)) 432 | if glob['ignore_conflicts_dependencies'] is True: 433 | print('Ignoring...') 434 | else: 435 | print('Stopping here !') 436 | exit(0) 437 | 438 | # Install required / optional dependencies 439 | if glob['install_required_dependencies'] is True: 440 | install_dependencies(mod_name, dependencies, "required") 441 | 442 | # Check for optional dependencies if needed 443 | if install_optional_dependencies is True and glob['install_optional_dependencies'] is True: 444 | install_dependencies(mod_name, dependencies, "optional") 445 | 446 | # Add the mod to the global list of mods which will be written to "mod-list.json" later 447 | add_to_glob_mod_list(mod) 448 | 449 | # Check if file already exists and have the same sha1 450 | file_path = os.path.join(glob['mods_folder_path'], target_release['file_name']) 451 | if check_file_and_sha(file_path, target_release['sha1']): 452 | return 453 | 454 | # Download the file 455 | debug('Downloading mod %s' % (mod_infos['name'])) 456 | file_path = os.path.join(glob['mods_folder_path'], target_release['file_name']) 457 | download_mod(file_path, target_release['download_url']) 458 | 459 | print('Installed mod %s version %s for Factorio version %s' % ( 460 | mod_name, 461 | target_release['version'], 462 | target_release['info_json']['factorio_version'] 463 | )) 464 | 465 | # Save globally that a reload of Factorio is needed in the end. 466 | glob['has_to_reload'] = True 467 | 468 | return True 469 | 470 | 471 | def install_dependencies(parent_name, dependencies, dependencies_type): 472 | # Install required / optional dependencies 473 | for dependency in dependencies[dependencies_type]: 474 | print('Installing %s dependency "%s" version >= "%s" for "%s"' % ( 475 | dependencies_type, 476 | dependency[0], 477 | dependency[1], 478 | parent_name 479 | )) 480 | # Install the dependency and set the flag for optional dependencies (of the dependency) to False 481 | # Optional dependencies of a dependency should be installed by doing 'mod_manager.py -i $mod_name$ -iod' 482 | # where $mod_name$ is name of the optional dependency 483 | install_mod(dependency[0], dependency[1], False) 484 | 485 | 486 | glob_remove_mod_seen = {} 487 | 488 | 489 | def remove_mod(mod_name, remove_optional_dependencies=True): 490 | print('Removing "%s"' % mod_name) 491 | global glob_remove_mod_seen 492 | 493 | if mod_name in glob_remove_mod_seen: 494 | print('Mod %s already removed, skipping...' % mod_name) 495 | return 496 | 497 | glob_remove_mod_seen[mod_name] = True 498 | 499 | mod = { 500 | 'name': mod_name, 501 | 'enabled': True 502 | } 503 | 504 | mod_infos = get_mod_infos(mod) 505 | if not mod_infos or len(mod_infos['same_version_releases']) == 0: 506 | print('No matching version found for the mod "%s". Skipping...' % (mod['name'])) 507 | return False 508 | 509 | # Filter the one release we'll use 510 | target_release = mod_infos['same_version_releases'][0] 511 | 512 | if glob['remove_required_dependencies'] is True or \ 513 | (glob['remove_optional_dependencies'] is True and remove_optional_dependencies is True): 514 | 515 | dependencies = parse_dependencies(target_release['info_json']['dependencies']) 516 | if glob['remove_required_dependencies'] is True: 517 | remove_dependencies(mod_name, dependencies, "required") 518 | 519 | # Check for optional dependencies if needed 520 | if remove_optional_dependencies is True and glob['remove_optional_dependencies'] is True: 521 | remove_dependencies(mod_name, dependencies, "optional") 522 | 523 | if mod_infos is not None and 'releases' in mod_infos: 524 | for release in mod_infos['releases']: 525 | file_path = os.path.join(glob['mods_folder_path'], release['file_name']) 526 | remove_file(file_path) 527 | else: 528 | debug('No releases found for the mod "%s" skipping...' % mod_name) 529 | return False 530 | 531 | # We remove the mod from the global list of installed mods, 532 | # 'mod-list.json' file will be written later 533 | remove_to_glob_mod_list(mod) 534 | 535 | # Save globally that a reload of Factorio is needed in the end. 536 | glob['has_to_reload'] = True 537 | 538 | 539 | def remove_dependencies(parent_name, dependencies, dependencies_type): 540 | # Remove required / optional dependencies 541 | for dependency in dependencies[dependencies_type]: 542 | print('Removing "%s", %s dependency of "%s"' % ( 543 | dependency[0], 544 | dependencies_type, 545 | parent_name 546 | )) 547 | # Remove the dependency but NOT its own optional dependencies. 548 | # Optional dependencies of a dependency should be removed by doing 'mod_manager.py -r $mod_name$ -rod' 549 | # where $mod_name$ is name of the optional dependency 550 | remove_mod(dependency[0], False) 551 | 552 | 553 | def download_mod(file_path, download_url): 554 | if glob['dry_run']: 555 | print('Dry-running, would have downloaded (hiding credentials) : %s' % ('https://mods.factorio.com' + download_url)) 556 | return 557 | 558 | payload = {'username': glob['username'], 'token': glob['token']} 559 | r = requests.get('https://mods.factorio.com' + download_url, params=payload, stream=True) 560 | 561 | # the Factorio mod portal may serve downloads via a CDN, which 562 | # returns 'application/octet-stream' as the Content-Type 563 | if r.headers.get('Content-Type') != 'application/zip' and r.headers.get('Content-Type') != 'application/octet-stream' and r.headers.get('Content-Type') != 'binary/octet-stream': 564 | print('Error : Response is not a Zip file !') 565 | print('It might happen because your Username and/or Token are wrong or deactivated.') 566 | print('Aborting the mission...') 567 | exit(1) 568 | 569 | with open(file_path, 'wb') as fd: 570 | total_length = r.headers.get('content-length') 571 | if total_length is None: # no content length header 572 | fd.write(r.content) 573 | else: 574 | dl = 0 575 | total_length = int(total_length) 576 | for chunk in r.iter_content(8192): 577 | dl += len(chunk) 578 | fd.write(chunk) 579 | done = int(50 * dl / total_length) 580 | sys.stdout.write("\r[%s%s]" % ('=' * done, ' ' * (50 - done))) 581 | sys.stdout.flush() 582 | print() 583 | # We ensure all users can read the file (dirty fix case run as root...) 584 | os.chmod(file_path, 0o644) 585 | 586 | 587 | def update_state_mods(mods_name_list, should_enable): 588 | print('%s mod(s) %s' % ('Enabling' if should_enable else 'Disabling', mods_name_list)) 589 | 590 | for mod in mods_name_list: 591 | mod_list = { 592 | 'name': mod, 593 | 'enabled': should_enable 594 | } 595 | add_to_glob_mod_list(mod_list) 596 | 597 | # Save globally that a reload of Factorio is needed in the end. 598 | glob['has_to_reload'] = True 599 | 600 | 601 | def load_config(args): 602 | debug('Loading configuration...') 603 | try: 604 | with open(os.path.join(__location__, 'config.json'), 'r') as fd: 605 | config = json.load(fd) 606 | except FileNotFoundError: 607 | print("Couldn't load config file, as it didn't exist. Continuing with defaults anyway.") 608 | config = {} 609 | 610 | # GLIBC related 611 | glob['alternative_glibc_directory'] = args.alt_glibc_dir if args.alt_glibc_dir \ 612 | else (config['alternative_glibc_directory'] if "alternative_glibc_directory" in config else glob['alternative_glibc_directory']) 613 | glob['alternative_glibc_version'] = args.alt_glibc_version if args.alt_glibc_version \ 614 | else (config['alternative_glibc_version'] if "alternative_glibc_version" in config else glob['alternative_glibc_version']) 615 | 616 | if glob['alternative_glibc_directory'] is not False: 617 | # We check that if either of glibc params is set, the other is too. 618 | if (glob['alternative_glibc_directory'] is None and glob['alternative_glibc_version'] is not None) \ 619 | or (glob['alternative_glibc_directory'] is not None and glob['alternative_glibc_version'] is None): 620 | parser.error( 621 | 'The directory and version parameters for GLIBC must both have a value or not be specified at all. Got :\n' 622 | 'alternative-glibc-directory : %s\n' 623 | 'alternative-glibc-version : %s' 624 | % (glob['alternative_glibc_directory'], glob['alternative_glibc_version']) 625 | ) 626 | if glob['alternative_glibc_directory'] is not None and not os.path.isdir(glob['alternative_glibc_directory']): 627 | parser.error('The directory "%s" for the alternative GLIBC library points to nothing !' % glob['alternative_glibc_directory']) 628 | 629 | glibc_lib_file = "%s/lib/ld-%s.so" % (glob['alternative_glibc_directory'], glob['alternative_glibc_version']) 630 | if glob['alternative_glibc_directory'] is not None and not os.path.isfile(glibc_lib_file): 631 | parser.error( 632 | 'Could not find the GLIBC lib file corresponding to version %s ! The file "%s" must exists.' % 633 | (glob['alternative_glibc_version'], glibc_lib_file) 634 | ) 635 | 636 | # Service related 637 | glob['should_reload'] = args.should_reload if args.should_reload is True \ 638 | else (config['should_reload'] if "should_reload" in config else glob['should_reload']) 639 | glob['service_name'] = args.service_name if args.service_name is not None \ 640 | else (config['service_name'] if "service_name" in config else glob['service_name']) 641 | 642 | if glob['should_reload'] is True and glob['service_name'] is None: 643 | parser.error('Reload of Factorio is enabled but no service name was given. Set it in "config.json" or by passing -s argument.') 644 | 645 | # Path related 646 | glob['factorio_path'] = os.path.abspath(args.factorio_path) if args.factorio_path is not None \ 647 | else (config['factorio_path'] if "factorio_path" in config else glob['factorio_path']) 648 | if glob['factorio_path'] is None: 649 | parser.error('Factorio Path not correctly set. Set it in "config.json" or by passing -p argument.') 650 | 651 | glob['mods_folder_path'] = os.path.join(glob['factorio_path'], 'mods') 652 | if not os.path.exists(glob['mods_folder_path']) and not os.path.isdir(glob['mods_folder_path']): 653 | print('Factorio mod folder cannot be found in %s' % (glob['mods_folder_path'])) 654 | return False 655 | 656 | glob['mods_list_path'] = os.path.join(glob['mods_folder_path'], 'mod-list.json') 657 | if not os.path.exists(glob['mods_list_path']) and not os.path.isfile(glob['mods_list_path']): 658 | print('Factorio mod list file cannot be found in %s' % (glob['mods_list_path'])) 659 | return False 660 | 661 | # User credential related 662 | glob['username'] = args.username if args.username is not None \ 663 | else (config['username'] if "username" in config else glob['username']) 664 | glob['token'] = args.token if args.token is not None \ 665 | else (config['token'] if "token" in config else glob['token']) 666 | 667 | # If we are updating OR there is a mod to install, we ensure that the username and token are set 668 | if (args.should_update is True or args.mod_name_to_install is not None) and (glob['username'] is None or glob['username'] is None): 669 | parser.error('Username and/or Token not correctly set. Set them in "config.json" or by passing -u / -t arguments. See README on how to obtain them.') 670 | 671 | # Script configuration related 672 | glob['verbose'] = args.verbose if args.verbose is not None \ 673 | else (config['verbose'] if "verbose" in config else glob['verbose']) 674 | glob['dry_run'] = args.dry_run if args.dry_run is not None else glob['dry_run'] 675 | glob['factorio_version'] = find_version() 676 | glob['should_downgrade'] = args.should_downgrade if args.should_downgrade is not None \ 677 | else (config['should_downgrade'] if "should_downgrade" in config else glob['should_downgrade']) 678 | 679 | # Dependencies related 680 | glob['install_required_dependencies'] = False if args.disable_required_dependencies is True \ 681 | else (config['install_required_dependencies'] if "install_required_dependencies" in config else glob['install_required_dependencies']) 682 | glob['install_optional_dependencies'] = True if args.install_optional_dependencies is True \ 683 | else (config['install_optional_dependencies'] if "install_optional_dependencies" in config else glob['install_optional_dependencies']) 684 | 685 | glob['remove_required_dependencies'] = False if args.remove_required_dependencies is False \ 686 | else (config['remove_required_dependencies'] if "remove_required_dependencies" in config else glob['remove_required_dependencies']) 687 | glob['remove_optional_dependencies'] = True if args.remove_optional_dependencies is True \ 688 | else (config['remove_optional_dependencies'] if "remove_optional_dependencies" in config else glob['remove_optional_dependencies']) 689 | 690 | glob['ignore_conflicts_dependencies'] = True if args.ignore_conflicts_dependencies is True \ 691 | else (config['ignore_conflicts_dependencies'] if "ignore_conflicts_dependencies" in config else glob['ignore_conflicts_dependencies']) 692 | 693 | return True 694 | 695 | 696 | def debug(string): 697 | if glob['verbose'] is True: 698 | print('Debug: ' + string, end='\n\n') 699 | 700 | 701 | def check_mod_manager_update(): 702 | print('Checking for updates...') 703 | # Python 2 compatibility to silence the call 704 | try: 705 | with open(os.devnull, 'wb') as shutup: 706 | return_code = subprocess.call(["git", "pull", "--quiet", "--ff-only"], cwd=__location__, stdout=shutup, stderr=shutup) 707 | 708 | if return_code != 0: 709 | print(""" 710 | ############################################################################################### 711 | An update of Factorio-mod-manager is available but cannot be applied via git-pull ! Ignoring... 712 | ############################################################################################### 713 | """) 714 | else: 715 | print("No new version found, exiting...") 716 | except FileNotFoundError: 717 | print('Cannot find "git" executable, skipping...') 718 | 719 | 720 | def main(): 721 | if len(sys.argv) == 1: 722 | parser.print_help() 723 | exit() 724 | 725 | args = parser.parse_args() 726 | 727 | # Check if an update (of Factorio-mod-manager) is available 728 | if args.update_mod_manager: 729 | check_mod_manager_update() 730 | exit(0) 731 | 732 | if not load_config(args): 733 | print('Failing miserably...') 734 | exit(1) 735 | 736 | # List installed mods 737 | if args.list_mods: 738 | display_mods_list(read_mods_list()) 739 | exit(0) 740 | 741 | # Enabled mods 742 | if args.enable_mods_name: 743 | update_state_mods(args.enable_mods_name, True) 744 | print() 745 | 746 | # Disabled mods 747 | if args.disable_mods_name: 748 | update_state_mods(args.disable_mods_name, False) 749 | print() 750 | 751 | # If we should update the mods 752 | if args.should_update: 753 | update_mods(args.enabled_only) 754 | print() 755 | 756 | # If there is a mod to install 757 | if args.mod_name_to_install: 758 | install_mod(args.mod_name_to_install) 759 | print() 760 | 761 | # If there is a mod to remove 762 | if args.remove_mod_name: 763 | remove_mod(args.remove_mod_name) 764 | print() 765 | 766 | write_mods_list() 767 | 768 | if glob['has_to_reload'] is True: 769 | print('The mod configuration changed and Factorio need to be restarted in order to apply the changes.') 770 | 771 | if glob['dry_run']: 772 | print('Dry-running, would have%s automatically reloaded' % (" NOT" if glob['should_reload'] is False else "")) 773 | return 774 | 775 | if glob['should_reload'] is True: 776 | print('Reloading service %s' % (glob['service_name'])) 777 | os.system('systemctl restart %s' % (glob['service_name'])) 778 | else: 779 | print('Automatic reload has been disabled, please restart Factorio by yourself.') 780 | 781 | print('Finished !') 782 | exit(0) 783 | 784 | 785 | if __name__ == '__main__': 786 | sys.exit(main()) 787 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2024.7.4 2 | chardet==4.0.0 3 | charset-normalizer==2.0.12 4 | idna==3.7 5 | packaging==21.3 6 | pyparsing==3.0.9 7 | requests==2.32.4 8 | urllib3==1.26.19 9 | --------------------------------------------------------------------------------