mirror of
https://github.com/derrod/legendary.git
synced 2024-12-22 01:45:28 +00:00
[cli/core/utils] Add experimental automatic bottle setup
Not sure if this will make it into the release yet, but it doesn't seem like a bad idea. And it should work even if the user has never run CrossOver. It's quite a lot of work to package a bottle this way (read: not including personal data, and without broken symlinks)
This commit is contained in:
parent
8512a9a7a1
commit
013792f7b9
|
@ -22,7 +22,9 @@ from legendary.core import LegendaryCore
|
||||||
from legendary.models.exceptions import InvalidCredentialsError
|
from legendary.models.exceptions import InvalidCredentialsError
|
||||||
from legendary.models.game import SaveGameStatus, VerifyResult, Game
|
from legendary.models.game import SaveGameStatus, VerifyResult, Game
|
||||||
from legendary.utils.cli import get_boolean_choice, get_int_choice, sdl_prompt, strtobool
|
from legendary.utils.cli import get_boolean_choice, get_int_choice, sdl_prompt, strtobool
|
||||||
from legendary.utils.crossover import mac_find_crossover_apps, mac_get_crossover_bottles, mac_is_valid_bottle
|
from legendary.utils.crossover import (
|
||||||
|
mac_find_crossover_apps, mac_get_crossover_bottles, mac_is_valid_bottle, mac_is_crossover_running
|
||||||
|
)
|
||||||
from legendary.utils.custom_parser import AliasedSubParsersAction
|
from legendary.utils.custom_parser import AliasedSubParsersAction
|
||||||
from legendary.utils.env import is_windows_mac_or_pyi
|
from legendary.utils.env import is_windows_mac_or_pyi
|
||||||
from legendary.utils.eos import add_registry_entries, query_registry_entries, remove_registry_entries
|
from legendary.utils.eos import add_registry_entries, query_registry_entries, remove_registry_entries
|
||||||
|
@ -551,7 +553,7 @@ class LegendaryCLI:
|
||||||
|
|
||||||
# Interactive CrossOver setup
|
# Interactive CrossOver setup
|
||||||
if args.crossover and sys_platform == 'darwin':
|
if args.crossover and sys_platform == 'darwin':
|
||||||
args.reset = False
|
args.reset = args.download = False
|
||||||
self.crossover_setup(args)
|
self.crossover_setup(args)
|
||||||
|
|
||||||
if args.origin:
|
if args.origin:
|
||||||
|
@ -2125,12 +2127,82 @@ class LegendaryCLI:
|
||||||
f'for setup instructions')
|
f'for setup instructions')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
forced_selection = None
|
||||||
bottles = mac_get_crossover_bottles()
|
bottles = mac_get_crossover_bottles()
|
||||||
if 'Legendary' not in bottles:
|
# todo support names other than Legendary for downloaded bottles
|
||||||
|
if 'Legendary' not in bottles and not args.download:
|
||||||
logger.info('It is recommended to set up a bottle specifically for Legendary, see '
|
logger.info('It is recommended to set up a bottle specifically for Legendary, see '
|
||||||
'https://legendary.gl/crossover-setup for setup instructions.')
|
'https://legendary.gl/crossover-setup for setup instructions.')
|
||||||
|
elif 'Legendary' in bottles and args.download:
|
||||||
|
logger.info('Legendary is already installed in a bottle, skipping download.')
|
||||||
|
forced_selection = 'Legendary'
|
||||||
|
elif args.download:
|
||||||
|
if mac_is_crossover_running():
|
||||||
|
logger.error('CrossOver is still running, please quit it before proceeding.')
|
||||||
|
return
|
||||||
|
|
||||||
if len(bottles) > 1:
|
logger.info('Checking available bottles...')
|
||||||
|
available_bottles = self.core.get_available_bottles()
|
||||||
|
usable_bottles = [b for b in available_bottles if b['cx_version'] == cx_version]
|
||||||
|
logger.info(f'Found {len(usable_bottles)} bottles usable with the selected CrossOver version. '
|
||||||
|
f'(Total: {len(available_bottles)})')
|
||||||
|
|
||||||
|
if len(usable_bottles) == 0:
|
||||||
|
logger.info(f'No usable bottles found, see https://legendary.gl/crossover-setup for '
|
||||||
|
f'manual setup instructions.')
|
||||||
|
install_candidate = None
|
||||||
|
elif len(usable_bottles) == 1:
|
||||||
|
install_candidate = usable_bottles[0]
|
||||||
|
else:
|
||||||
|
print('Found multiple available bottles, please select one:')
|
||||||
|
|
||||||
|
default_choice = None
|
||||||
|
for i, bottle in enumerate(usable_bottles, start=1):
|
||||||
|
if bottle['is_default']:
|
||||||
|
default_choice = i
|
||||||
|
print(f'\t{i:2d}. {bottle["name"]} ({bottle["description"]}) [default]')
|
||||||
|
else:
|
||||||
|
print(f'\t{i:2d}. {bottle["name"]} ({bottle["description"]})')
|
||||||
|
|
||||||
|
choice = get_int_choice(f'Select a bottle', default_choice, 1, len(usable_bottles))
|
||||||
|
if choice is None:
|
||||||
|
logger.error(f'No valid choice made, aborting.')
|
||||||
|
return
|
||||||
|
|
||||||
|
install_candidate = usable_bottles[choice - 1]
|
||||||
|
|
||||||
|
if install_candidate:
|
||||||
|
logger.info(f'Preparing to download "{install_candidate["name"]}" '
|
||||||
|
f'({install_candidate["description"]})...')
|
||||||
|
dlm, ares, path = self.core.prepare_bottle_download(install_candidate['name'],
|
||||||
|
install_candidate['manifest'])
|
||||||
|
|
||||||
|
logger.info(f'Bottle install directory: {path}')
|
||||||
|
logger.info(f'Bottle size: {ares.install_size / 1024 / 1024:.2f} MiB')
|
||||||
|
logger.info(f'Download size: {ares.dl_size / 1024 / 1024:.2f} MiB')
|
||||||
|
|
||||||
|
if not args.yes:
|
||||||
|
if not get_boolean_choice('Do you want to download the selected bottle?'):
|
||||||
|
print('Aborting...')
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# set up logging stuff (should be moved somewhere else later)
|
||||||
|
dlm.logging_queue = self.logging_queue
|
||||||
|
dlm.start()
|
||||||
|
dlm.join()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'The following exception occurred while waiting for the downloader: {e!r}. '
|
||||||
|
f'Try restarting the process, if it continues to fail please open an issue on GitHub.')
|
||||||
|
# delete the unfinished bottle
|
||||||
|
self.core.remove_bottle(install_candidate['name'])
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.info('Finished downloading, finalising bottle setup...')
|
||||||
|
self.core.finish_bottle_setup(install_candidate['name'])
|
||||||
|
forced_selection = install_candidate['name']
|
||||||
|
|
||||||
|
if len(bottles) > 1 and not forced_selection:
|
||||||
print('Found multiple CrossOver bottles, please select one:')
|
print('Found multiple CrossOver bottles, please select one:')
|
||||||
|
|
||||||
if 'Legendary' in bottles:
|
if 'Legendary' in bottles:
|
||||||
|
@ -2154,9 +2226,11 @@ class LegendaryCLI:
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
args.crossover_bottle = bottles[choice - 1]
|
args.crossover_bottle = bottles[choice - 1]
|
||||||
elif len(bottles) == 1:
|
elif len(bottles) == 1 and not forced_selection:
|
||||||
logger.info(f'Found only one bottle: {bottles[0]}')
|
logger.info(f'Found only one bottle: {bottles[0]}')
|
||||||
args.crossover_bottle = bottles[0]
|
args.crossover_bottle = bottles[0]
|
||||||
|
elif forced_selection:
|
||||||
|
args.crossover_bottle = forced_selection
|
||||||
else:
|
else:
|
||||||
logger.error('No Bottles found, see https://legendary.gl/crossover-setup for setup instructions.')
|
logger.error('No Bottles found, see https://legendary.gl/crossover-setup for setup instructions.')
|
||||||
return
|
return
|
||||||
|
@ -2516,6 +2590,8 @@ def main():
|
||||||
|
|
||||||
cx_parser.add_argument('--reset', dest='reset', action='store_true',
|
cx_parser.add_argument('--reset', dest='reset', action='store_true',
|
||||||
help='Reset default/app-specific crossover configuration')
|
help='Reset default/app-specific crossover configuration')
|
||||||
|
cx_parser.add_argument('--download', dest='download', action='store_true',
|
||||||
|
help='Automatically download and set up a preconfigured bottle (experimental)')
|
||||||
|
|
||||||
args, extra = parser.parse_known_args()
|
args, extra = parser.parse_known_args()
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ from legendary.models.game import *
|
||||||
from legendary.models.json_manifest import JSONManifest
|
from legendary.models.json_manifest import JSONManifest
|
||||||
from legendary.models.manifest import Manifest, ManifestMeta
|
from legendary.models.manifest import Manifest, ManifestMeta
|
||||||
from legendary.models.chunk import Chunk
|
from legendary.models.chunk import Chunk
|
||||||
from legendary.utils.crossover import mac_find_crossover_apps, mac_get_crossover_version
|
from legendary.utils.crossover import mac_find_crossover_apps, mac_get_crossover_version, EMPTY_BOTTLE_DIRECTORIES
|
||||||
from legendary.utils.egl_crypt import decrypt_epic_data
|
from legendary.utils.egl_crypt import decrypt_epic_data
|
||||||
from legendary.utils.env import is_windows_mac_or_pyi
|
from legendary.utils.env import is_windows_mac_or_pyi
|
||||||
from legendary.utils.eos import EOSOverlayApp, query_registry_entries
|
from legendary.utils.eos import EOSOverlayApp, query_registry_entries
|
||||||
|
@ -1828,6 +1828,76 @@ class LegendaryCore:
|
||||||
delete_folder(igame.install_path, recursive=True)
|
delete_folder(igame.install_path, recursive=True)
|
||||||
self.lgd.remove_overlay_install_info()
|
self.lgd.remove_overlay_install_info()
|
||||||
|
|
||||||
|
def get_available_bottles(self):
|
||||||
|
self.check_for_updates(force=True)
|
||||||
|
lgd_version_data = self.lgd.get_cached_version()
|
||||||
|
return lgd_version_data.get('data', {}).get('cx_bottles', [])
|
||||||
|
|
||||||
|
def prepare_bottle_download(self, bottle_name, manifest_url):
|
||||||
|
r = self.egs.unauth_session.get(manifest_url)
|
||||||
|
r.raise_for_status()
|
||||||
|
manifest = self.load_manifest(r.content)
|
||||||
|
base_url = manifest_url.rpartition('/')[0]
|
||||||
|
|
||||||
|
bottles_dir = os.path.expanduser('~/Library/Application Support/CrossOver/Bottles')
|
||||||
|
path = os.path.join(bottles_dir, bottle_name)
|
||||||
|
|
||||||
|
if os.path.exists(path):
|
||||||
|
raise FileExistsError(f'Bottle {bottle_name} already exists')
|
||||||
|
|
||||||
|
dlm = DLManager(path, base_url)
|
||||||
|
analysis_result = dlm.run_analysis(manifest=manifest)
|
||||||
|
|
||||||
|
install_size = analysis_result.install_size
|
||||||
|
|
||||||
|
parent_dir = path
|
||||||
|
while not os.path.exists(parent_dir):
|
||||||
|
parent_dir, _ = os.path.split(parent_dir)
|
||||||
|
|
||||||
|
_, _, free = shutil.disk_usage(parent_dir)
|
||||||
|
if free < install_size:
|
||||||
|
raise ValueError(f'Not enough space to setup bottle: {free / 1024 / 1024:.02f} '
|
||||||
|
f'MiB < {install_size / 1024 / 1024:.02f} MiB')
|
||||||
|
|
||||||
|
return dlm, analysis_result, path
|
||||||
|
|
||||||
|
def finish_bottle_setup(self, bottle_name):
|
||||||
|
bottles_dir = os.path.expanduser('~/Library/Application Support/CrossOver/Bottles')
|
||||||
|
path = os.path.join(bottles_dir, bottle_name)
|
||||||
|
|
||||||
|
self.log.info('Creating missing folders...')
|
||||||
|
os.makedirs(os.path.join(path, 'dosdevices'), exist_ok=True)
|
||||||
|
for _dir in EMPTY_BOTTLE_DIRECTORIES:
|
||||||
|
os.makedirs(os.path.join(path, 'drive_c', _dir), exist_ok=True)
|
||||||
|
|
||||||
|
self.log.info('Creating bottle symlinks...')
|
||||||
|
symlinks = [
|
||||||
|
('dosdevices/c:', '../drive_c'),
|
||||||
|
('dosdevices/y:', os.path.expanduser('~')),
|
||||||
|
('dosdevices/z:', '/'),
|
||||||
|
('drive_c/users/crossover/Desktop/My Mac Desktop', os.path.expanduser('~/Desktop')),
|
||||||
|
('drive_c/users/crossover/Downloads', os.path.expanduser('~/Downloads')),
|
||||||
|
('drive_c/users/crossover/My Documents', os.path.expanduser('~/Documents')),
|
||||||
|
('drive_c/users/crossover/My Music', os.path.expanduser('~/Music')),
|
||||||
|
('drive_c/users/crossover/My Pictures', os.path.expanduser('~/Pictures')),
|
||||||
|
('drive_c/users/crossover/My Videos', os.path.expanduser('~/Movies')),
|
||||||
|
('drive_c/users/crossover/Templates', os.path.join(path, 'dosdevices/c:/users/crossover/My Documents')),
|
||||||
|
]
|
||||||
|
|
||||||
|
for link, target in symlinks:
|
||||||
|
_link = os.path.join(path, link)
|
||||||
|
try:
|
||||||
|
os.symlink(target, _link)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(f'Failed to create symlink {_link} -> {target}: {e!r}')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def remove_bottle(self, bottle_name):
|
||||||
|
bottles_dir = os.path.expanduser('~/Library/Application Support/CrossOver/Bottles')
|
||||||
|
path = os.path.join(bottles_dir, bottle_name)
|
||||||
|
if os.path.exists(path):
|
||||||
|
delete_folder(path, recursive=True)
|
||||||
|
|
||||||
def exit(self):
|
def exit(self):
|
||||||
"""
|
"""
|
||||||
Do cleanup, config saving, and exit.
|
Do cleanup, config saving, and exit.
|
||||||
|
|
|
@ -5,6 +5,50 @@ import subprocess
|
||||||
|
|
||||||
logger = logging.getLogger('CXHelpers')
|
logger = logging.getLogger('CXHelpers')
|
||||||
|
|
||||||
|
# all the empty folders found in a freshly created bottle that we will need to create
|
||||||
|
EMPTY_BOTTLE_DIRECTORIES = [
|
||||||
|
'Program Files/Common Files/Microsoft Shared/TextConv',
|
||||||
|
'ProgramData/Microsoft/Windows/Start Menu/Programs/Administrative Tools',
|
||||||
|
'ProgramData/Microsoft/Windows/Start Menu/Programs/StartUp',
|
||||||
|
'ProgramData/Microsoft/Windows/Templates',
|
||||||
|
'users/crossover/AppData/LocalLow',
|
||||||
|
'users/crossover/Application Data/Microsoft/Windows/Themes',
|
||||||
|
'users/crossover/Contacts',
|
||||||
|
'users/crossover/Cookies',
|
||||||
|
'users/crossover/Desktop',
|
||||||
|
'users/crossover/Favorites',
|
||||||
|
'users/crossover/Links',
|
||||||
|
'users/crossover/Local Settings/Application Data/Microsoft',
|
||||||
|
'users/crossover/Local Settings/History',
|
||||||
|
'users/crossover/Local Settings/Temporary Internet Files',
|
||||||
|
'users/crossover/NetHood',
|
||||||
|
'users/crossover/PrintHood',
|
||||||
|
'users/crossover/Recent',
|
||||||
|
'users/crossover/Saved Games',
|
||||||
|
'users/crossover/Searches',
|
||||||
|
'users/crossover/SendTo',
|
||||||
|
'users/crossover/Start Menu/Programs/Administrative Tools',
|
||||||
|
'users/crossover/Start Menu/Programs/StartUp',
|
||||||
|
'users/crossover/Temp',
|
||||||
|
'users/Public/Desktop',
|
||||||
|
'users/Public/Documents',
|
||||||
|
'users/Public/Favorites',
|
||||||
|
'users/Public/Music',
|
||||||
|
'users/Public/Pictures',
|
||||||
|
'users/Public/Videos',
|
||||||
|
'windows/Fonts',
|
||||||
|
'windows/help',
|
||||||
|
'windows/logs',
|
||||||
|
'windows/Microsoft.NET/DirectX for Managed Code',
|
||||||
|
'windows/system32/mui',
|
||||||
|
'windows/system32/spool/printers',
|
||||||
|
'windows/system32/tasks',
|
||||||
|
'windows/syswow64/drivers',
|
||||||
|
'windows/syswow64/mui',
|
||||||
|
'windows/tasks',
|
||||||
|
'windows/temp'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def mac_get_crossover_version(app_path):
|
def mac_get_crossover_version(app_path):
|
||||||
try:
|
try:
|
||||||
|
@ -48,3 +92,12 @@ def mac_get_crossover_bottles():
|
||||||
def mac_is_valid_bottle(bottle_name):
|
def mac_is_valid_bottle(bottle_name):
|
||||||
bottles_path = os.path.expanduser('~/Library/Application Support/CrossOver/Bottles')
|
bottles_path = os.path.expanduser('~/Library/Application Support/CrossOver/Bottles')
|
||||||
return os.path.exists(os.path.join(bottles_path, bottle_name, 'cxbottle.conf'))
|
return os.path.exists(os.path.join(bottles_path, bottle_name, 'cxbottle.conf'))
|
||||||
|
|
||||||
|
|
||||||
|
def mac_is_crossover_running():
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(['launchctl', 'list'])
|
||||||
|
return b'com.codeweavers.CrossOver' in out
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f'Getting list of running application bundles failed: {e!r}')
|
||||||
|
return True # assume the worst
|
||||||
|
|
Loading…
Reference in a new issue