forked from ynput/ayon-launcher
-
Notifications
You must be signed in to change notification settings - Fork 0
/
start.py
990 lines (803 loc) · 30.3 KB
/
start.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
# -*- coding: utf-8 -*-
"""Main entry point for AYON command.
Bootstrapping process of AYON.
This script is responsible for setting up the environment and
bootstraping AYON. It is also responsible for updating AYON
from AYON server.
Arguments that are always handled by AYON launcher:
--verbose <level> - set log level
--debug - enable debug mode
--skip-headers - skip printing headers
--skip-bootstrap - skip bootstrap process - use only for bootstrap logic
--use-staging - use staging server
--use-dev - use dev server
--bundle <bundle_name> - specify bundle name to use
--headless - enable headless mode - bootstrap won't show any UI
AYON launcher can be running in multiple different states. The top layer of
states is 'production', 'staging' and 'dev'.
To start in dev mode use one of following options:
- by passing '--use-dev' argument
- by setting 'AYON_USE_DEV' environment variable to '1'
- by passing '--bundle <dev bundle name>'
- by setting 'AYON_BUNDLE_NAME' environment variable to dev bundle name
NOTE: By using bundle name you can start any dev bundle, even if is not
assigned to current user.
To start in staging mode make sure none of develop options are used and then
use one of following options:
- by passing '--use-staging' argument
- by setting 'AYON_USE_STAGING' environment variable to '1'
Staging mode must be defined explicitly cannot be determined by bundle name.
In all other cases AYON launcher will start in 'production' mode.
Headless mode is not guaranteed after bootstrap process. It is possible that
some addon won't handle headless mode and will try to use UIs.
After bootstrap process AYON launcher will start 'openpype' addon. This addon
is responsible for handling all other addons and their logic.
Environment variables set during bootstrap:
- AYON_VERSION - version of AYON launcher
- AYON_BUNDLE_NAME - name of bundle to use
- AYON_USE_STAGING - set to '1' if staging mode is enabled
- AYON_USE_DEV - set to '1' if dev mode is enabled
- AYON_DEBUG - set to '1' if debug mode is enabled
- AYON_HEADLESS_MODE - set to '1' if headless mode is enabled
- AYON_SERVER_URL - URL of AYON server
- AYON_API_KEY - API key for AYON server
- AYON_SERVER_TIMEOUT - timeout for AYON server
- AYON_SERVER_RETRIES - number of retries for AYON server
- AYON_EXECUTABLE - path to AYON executable
- AYON_ROOT - path to AYON root directory
- AYON_MENU_LABEL - label for AYON integrations menu
- AYON_ADDONS_DIR - path to AYON addons directory
- AYON_DEPENDENCIES_DIR - path to AYON dependencies directory
OpenPype environment variables set during bootstrap
for backward compatibility:
- PYBLISH_GUI - default pyblish UI tool - will be removed in future
- USE_AYON_SERVER - tells openpype addon to run in AYON mode
- AVALON_LABEL - label for AYON menu
- OPENPYPE_VERSION - version of OpenPype addon
- OPENPYPE_USE_STAGING - set to '1' if staging mode is enabled
- OPENPYPE_DEBUG - set to '1' if debug mode is enabled
- OPENPYPE_HEADLESS_MODE - set to '1' if headless mode is enabled
- OPENPYPE_EXECUTABLE - path to OpenPype executable
- OPENPYPE_ROOT - path to OpenPype root directory
- OPENPYPE_REPOS_ROOT - path to OpenPype repos root directory
- OPENPYPE_LOG_LEVEL - log level for OpenPype
Some of the environment variables are not in this script but in 'ayon_common'
module.
- Function 'create_global_connection' can change 'AYON_USE_DEV' and
'AYON_USE_STAGING'.
- Distribution logic can set 'AYON_ADDONS_DIR' and 'AYON_DEPENDENCIES_DIR'
if are not set yet.
"""
import os
import platform
import sys
import site
import traceback
import contextlib
import subprocess
from version import __version__
ORIGINAL_ARGS = list(sys.argv)
PREVIOUS_AYON_VERSION = os.getenv("AYON_VERSION", "")
os.environ["AYON_VERSION"] = __version__
# Define which bundle is used
if "--bundle" in sys.argv:
idx = sys.argv.index("--bundle")
sys.argv.pop(idx)
if idx >= len(sys.argv):
raise RuntimeError((
"Expect value after \"--bundle\" argument."
))
os.environ["AYON_BUNDLE_NAME"] = sys.argv.pop(idx)
# Enabled logging debug mode when "--debug" is passed
if "--verbose" in sys.argv:
expected_values = (
"Expected: notset, debug, info, warning, error, critical"
" or integer [0-50]."
)
idx = sys.argv.index("--verbose")
sys.argv.pop(idx)
if idx >= len(sys.argv):
raise RuntimeError((
f"Expect value after \"--verbose\" argument. {expected_values}"
))
value = sys.argv.pop(idx)
low_value = value.lower()
log_level = None
if low_value.isdigit():
log_level = int(low_value)
elif low_value == "notset":
log_level = 0
elif low_value == "debug":
log_level = 10
elif low_value == "info":
log_level = 20
elif low_value == "warning":
log_level = 30
elif low_value == "error":
log_level = 40
elif low_value == "critical":
log_level = 50
if log_level is None:
raise ValueError((
"Unexpected value after \"--verbose\" "
f"argument \"{value}\". {expected_values}"
))
os.environ["OPENPYPE_LOG_LEVEL"] = str(log_level)
os.environ["AYON_LOG_LEVEL"] = str(log_level)
# Enable debug mode, may affect log level if log level is not defined
if "--debug" in sys.argv:
sys.argv.remove("--debug")
os.environ["AYON_DEBUG"] = "1"
os.environ["OPENPYPE_DEBUG"] = "1"
SKIP_HEADERS = False
if "--skip-headers" in sys.argv:
sys.argv.remove("--skip-headers")
SKIP_HEADERS = True
SKIP_BOOTSTRAP = False
if "--skip-bootstrap" in sys.argv:
sys.argv.remove("--skip-bootstrap")
SKIP_BOOTSTRAP = True
if "--use-staging" in sys.argv:
sys.argv.remove("--use-staging")
os.environ["AYON_USE_STAGING"] = "1"
os.environ["OPENPYPE_USE_STAGING"] = "1"
if "--use-dev" in sys.argv:
sys.argv.remove("--use-dev")
os.environ["AYON_USE_DEV"] = "1"
SHOW_LOGIN_UI = False
if "--ayon-login" in sys.argv:
sys.argv.remove("--ayon-login")
SHOW_LOGIN_UI = True
def _is_in_login_mode():
# Handle cases when source AYON launcher has version before '1.0.1'
# - When user launcher an executable of AYON launcher it will run correct
# version of AYON launcher by bundle, but older launcher versions
# will not set 'AYON_IN_LOGIN_MODE' environment variable. Therefore
# we need to check 'PREVIOUS_AYON_VERSION' and set 'AYON_IN_LOGIN_MODE'
# to 'True' when version is before '1.0.1'.
# - this would be `return "AYON_API_KEY" not in os.environ` otherwise
if "AYON_API_KEY" not in os.environ:
return True
# Handle cases when source AYON launcher has version before '1.0.1'
version_parts = PREVIOUS_AYON_VERSION.split(".")
if len(version_parts) < 3:
return False
try:
# Keep only first 3 version parts which should be integers
new_version_parts = [
int(value)
for idx, value in enumerate(version_parts)
if idx < 3
]
except ValueError:
return False
milestone = (1, 0, 1)
return tuple(new_version_parts) < milestone
# Login mode is helper to detect if user is using AYON server credentials
# from login UI (and keyring), or from environment variables.
# - Variable is set in first AYON launcher process for possible subprocesses
if SHOW_LOGIN_UI:
# Make sure login mode is set to '1' when '--ayon-login' is passed
os.environ["AYON_IN_LOGIN_MODE"] = "1"
elif "AYON_IN_LOGIN_MODE" not in os.environ:
os.environ["AYON_IN_LOGIN_MODE"] = str(int(_is_in_login_mode()))
if "--headless" in sys.argv:
os.environ["AYON_HEADLESS_MODE"] = "1"
os.environ["OPENPYPE_HEADLESS_MODE"] = "1"
sys.argv.remove("--headless")
elif (
os.getenv("AYON_HEADLESS_MODE") != "1"
or os.getenv("OPENPYPE_HEADLESS_MODE") != "1"
):
os.environ.pop("AYON_HEADLESS_MODE", None)
os.environ.pop("OPENPYPE_HEADLESS_MODE", None)
elif (
os.getenv("AYON_HEADLESS_MODE")
!= os.getenv("OPENPYPE_HEADLESS_MODE")
):
os.environ["OPENPYPE_HEADLESS_MODE"] = (
os.environ["AYON_HEADLESS_MODE"]
)
IS_BUILT_APPLICATION = getattr(sys, "frozen", False)
HEADLESS_MODE_ENABLED = os.getenv("AYON_HEADLESS_MODE") == "1"
AYON_IN_LOGIN_MODE = os.environ["AYON_IN_LOGIN_MODE"] == "1"
_pythonpath = os.getenv("PYTHONPATH", "")
_python_paths = _pythonpath.split(os.pathsep)
if not IS_BUILT_APPLICATION:
# Code root defined by `start.py` directory
AYON_ROOT = os.path.dirname(os.path.abspath(__file__))
_dependencies_path = site.getsitepackages()[-1]
else:
AYON_ROOT = os.path.dirname(sys.executable)
# add dependencies folder to sys.pat for frozen code
_dependencies_path = os.path.normpath(
os.path.join(AYON_ROOT, "dependencies")
)
# add stuff from `<frozen>/dependencies` to PYTHONPATH.
sys.path.append(_dependencies_path)
_python_paths.append(_dependencies_path)
# Add common package to PYTHONPATH
# - common contains common code and bootstrap logic (like connection and bootstrap)
common_path = os.path.join(AYON_ROOT, "common")
sys.path.insert(0, common_path)
if common_path in _python_paths:
_python_paths.remove(common_path)
_python_paths.insert(0, common_path)
# Vendored python modules that must not be in PYTHONPATH environment but
# are required for OpenPype processes
sys.path.insert(0, os.path.join(AYON_ROOT, "vendor", "python"))
os.environ["PYTHONPATH"] = os.pathsep.join(_python_paths)
# enabled AYON state
os.environ["USE_AYON_SERVER"] = "1"
# Set this to point either to `python` from venv in case of live code
# or to `ayon` or `ayon_console` in case of frozen code
os.environ["AYON_EXECUTABLE"] = sys.executable
os.environ["OPENPYPE_EXECUTABLE"] = sys.executable
os.environ["AYON_ROOT"] = AYON_ROOT
os.environ["OPENPYPE_ROOT"] = AYON_ROOT
os.environ["OPENPYPE_REPOS_ROOT"] = AYON_ROOT
os.environ["AYON_MENU_LABEL"] = "AYON"
os.environ["AVALON_LABEL"] = "AYON"
import blessed # noqa: E402
import certifi # noqa: E402
if sys.__stdout__:
term = blessed.Terminal()
def _print(message: str):
if message.startswith("!!! "):
print(f'{term.orangered2("!!! ")}{message[4:]}')
elif message.startswith(">>> "):
print(f'{term.aquamarine3(">>> ")}{message[4:]}')
elif message.startswith("--- "):
print(f'{term.darkolivegreen3("--- ")}{message[4:]}')
elif message.startswith("*** "):
print(f'{term.gold("*** ")}{message[4:]}')
elif message.startswith(" - "):
print(f'{term.wheat(" - ")}{message[4:]}')
elif message.startswith(" . "):
print(f'{term.tan(" . ")}{message[4:]}')
elif message.startswith(" - "):
print(f'{term.seagreen3(" - ")}{message[7:]}')
elif message.startswith(" ! "):
print(f'{term.goldenrod(" ! ")}{message[7:]}')
elif message.startswith(" * "):
print(f'{term.aquamarine1(" * ")}{message[7:]}')
elif message.startswith(" "):
print(f'{term.darkseagreen3(" ")}{message[4:]}')
else:
print(message)
else:
def _print(message: str):
print(message)
# if SSL_CERT_FILE is not set prior to OpenPype launch, we set it to point
# to certifi bundle to make sure we have reasonably new CA certificates.
if not os.getenv("SSL_CERT_FILE"):
os.environ["SSL_CERT_FILE"] = certifi.where()
elif os.getenv("SSL_CERT_FILE") != certifi.where():
_print("--- your system is set to use custom CA certificate bundle.")
from ayon_api import (
get_base_url,
set_default_settings_variant,
get_addons_studio_settings,
)
from ayon_api.constants import (
SERVER_URL_ENV_KEY,
SERVER_API_ENV_KEY,
DEFAULT_VARIANT_ENV_KEY,
SITE_ID_ENV_KEY,
)
from ayon_common import is_staging_enabled, is_dev_mode_enabled
from ayon_common.connection.credentials import (
ask_to_login_ui,
add_server,
need_server_or_login,
load_environments,
set_environments,
create_global_connection,
confirm_server_login,
show_invalid_credentials_ui,
)
from ayon_common.distribution import (
AyonDistribution,
BundleNotFoundError,
show_missing_bundle_information,
show_installer_issue_information,
UpdateWindowManager,
)
from ayon_common.utils import (
store_current_executable_info,
get_local_site_id,
)
from ayon_common.startup import show_startup_error
def set_global_environments() -> None:
"""Set global OpenPype's environments."""
import acre
from openpype.settings import get_general_environments
general_env = get_general_environments()
# first resolve general environment because merge doesn't expect
# values to be list.
# TODO: switch to OpenPype environment functions
merged_env = acre.merge(
acre.compute(acre.parse(general_env), cleanup=False),
dict(os.environ)
)
env = acre.compute(
merged_env,
cleanup=False
)
os.environ.clear()
os.environ.update(env)
# Hardcoded default values
os.environ["PYBLISH_GUI"] = "pyblish_pype"
# Change scale factor only if is not set
if "QT_AUTO_SCREEN_SCALE_FACTOR" not in os.environ:
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
def set_addons_environments():
"""Set global environments for OpenPype modules.
This requires to have OpenPype in `sys.path`.
"""
import acre
from openpype.modules import ModulesManager
modules_manager = ModulesManager()
# Merge environments with current environments and update values
if module_envs := modules_manager.collect_global_environments():
parsed_envs = acre.parse(module_envs)
env = acre.merge(parsed_envs, dict(os.environ))
os.environ.clear()
os.environ.update(env)
def _connect_to_ayon_server(force=False):
"""Connect to AYON server.
Load existing credentials to AYON server, and show login dialog if are not
valid. When 'force' is set to 'True' then login dialog is always
shown.
Login dialog cannot be shown in headless mode. In that case program
is terminated with.
If user closed dialog, program is terminated with exit code 0.
Args:
force (Optional[bool]): Force login to server.
"""
if force and HEADLESS_MODE_ENABLED:
_print("!!! Login UI was requested in headless mode.")
sys.exit(1)
load_environments()
need_server = need_api_key = True
if not force:
need_server, need_api_key = need_server_or_login()
current_url = os.environ.get(SERVER_URL_ENV_KEY)
if not need_server and not need_api_key:
_print(f">>> Connected to AYON server {current_url}")
return
if need_server:
if current_url:
message = f"Could not connect to AYON server '{current_url}'."
else:
message = "AYON Server URL is not set."
elif os.environ.get(SERVER_API_ENV_KEY):
message = f"Invalid API key for '{current_url}'."
else:
message = f"Missing API key for '{current_url}'."
if not force:
_print("!!! Got invalid credentials.")
_print(message)
# Exit in headless mode
if HEADLESS_MODE_ENABLED:
_print((
f"!!! Please use '{SERVER_URL_ENV_KEY}'"
f" and '{SERVER_API_ENV_KEY}' environment variables to specify"
" valid server url and api key for headless mode."
))
sys.exit(1)
# Show message that used credentials are invalid
if not AYON_IN_LOGIN_MODE:
show_invalid_credentials_ui(message=message, in_subprocess=True)
sys.exit(1)
# Show login dialog
url, token, username = ask_to_login_ui(current_url, always_on_top=True)
if url is not None and token is not None:
confirm_server_login(url, token, username)
return
if url is not None:
add_server(url, username)
_print("!!! Login was not successful.")
sys.exit(0)
def _set_default_settings_variant(use_dev, use_staging, bundle_name):
"""Based on states set default settings variant.
Tell global connection which settings variant should be used.
Args:
use_dev (bool): Is dev mode enabled.
use_staging (bool): Is staging mode enabled.
bundle_name (str): Name of bundle to use.
"""
if use_dev:
variant = bundle_name
elif use_staging:
variant = "staging"
else:
variant = "production"
os.environ[DEFAULT_VARIANT_ENV_KEY] = variant
# Make sure dev env variable is set/unset for cases when dev mode is not
# enabled by '--use-dev' but by bundle name
if use_dev:
os.environ["AYON_USE_DEV"] = "1"
else:
os.environ.pop("AYON_USE_DEV", None)
# Make sure staging is unset when 'dev' should be used
if not use_staging:
os.environ.pop("AYON_USE_STAGING", None)
os.environ.pop("OPENPYPE_USE_STAGING", None)
set_default_settings_variant(variant)
def _prepare_disk_mapping_args(src_path, dst_path):
"""Prepare disk mapping arguments to run.
Args:
src_path (str): Source path.
dst_path (str): Destination path.
Returns:
list[str]: Arguments to run in subprocess.
"""
low_platform = platform.system().lower()
if low_platform == "windows":
dst_path = dst_path.replace("/", "\\").rstrip("\\")
src_path = src_path.replace("/", "\\").rstrip("\\")
# Add slash after ':' ('G:' -> 'G:\') only for source
if src_path.endswith(":"):
src_path += "\\"
return ["subst", dst_path, src_path]
dst_path = dst_path.rstrip("/")
src_path = src_path.rstrip("/")
if low_platform == "linux":
return ["sudo", "ln", "-s", src_path, dst_path]
if low_platform == "darwin":
scr = (
f'do shell script "ln -s {src_path} {dst_path}"'
' with administrator privileges'
)
return ["osascript", "-e", scr]
return []
def _run_disk_mapping(bundle_name):
"""Run disk mapping logic.
Mapping of disks is taken from core addon settings. To run this logic
'_set_default_settings_variant' must be called first, so correct
settings are received from server.
"""
low_platform = platform.system().lower()
settings = get_addons_studio_settings(bundle_name)
core_settings = settings.get("core") or {}
disk_mapping = core_settings.get("disk_mapping") or {}
platform_disk_mapping = disk_mapping.get(low_platform)
if not platform_disk_mapping:
return
for item in platform_disk_mapping:
src_path = item.get("source")
dst_path = item.get("destination")
if not src_path or not dst_path:
continue
if os.path.exists(dst_path):
continue
args = _prepare_disk_mapping_args(src_path, dst_path)
if not args:
continue
_print(f"*** disk mapping arguments: {args}")
try:
output = subprocess.Popen(args)
if output.returncode and output.returncode != 0:
exc_msg = f'Executing was not successful: "{args}"'
raise RuntimeError(exc_msg)
except TypeError as exc:
_print(
f"Error {str(exc)} in mapping drive {src_path}, {dst_path}")
raise
def _start_distribution():
"""Gets info from AYON server and updates possible missing pieces.
Raises:
RuntimeError
"""
# Create distribution object
distribution = AyonDistribution(
skip_installer_dist=not IS_BUILT_APPLICATION
)
bundle = None
bundle_name = None
# Try to find required bundle and handle missing one
try:
bundle = distribution.bundle_to_use
if bundle is not None:
bundle_name = bundle.name
except BundleNotFoundError as exc:
bundle_name = exc.bundle_name
if bundle is None:
url = get_base_url()
username = distribution.active_user
if bundle_name:
_print((
f"!!! Requested release bundle '{bundle_name}'"
" is not available on server."
))
_print(
"!!! Check if selected release bundle"
f" is available on the server '{url}'."
)
else:
mode = "production"
if distribution.use_dev:
mode = f"dev for user '{username}'"
elif distribution.use_staging:
mode = "staging"
_print(
f"!!! No release bundle is set as {mode} on the AYON server."
)
_print(
"!!! Make sure there is a release bundle set"
f" as \"{mode}\" on the AYON server '{url}'."
)
if not HEADLESS_MODE_ENABLED:
show_missing_bundle_information(url, bundle_name, username)
sys.exit(1)
# With known bundle and states we can define default settings variant
# in global connection
_set_default_settings_variant(
distribution.use_dev,
distribution.use_staging,
bundle_name
)
_run_disk_mapping(bundle_name)
# Start distribution
update_window_manager = UpdateWindowManager()
if not HEADLESS_MODE_ENABLED:
update_window_manager.start()
try:
distribution.distribute()
finally:
update_window_manager.stop()
if distribution.need_installer_change:
# Check if any error happened
error = distribution.installer_dist_error
if error:
if HEADLESS_MODE_ENABLED:
_print(error)
else:
show_installer_issue_information(
error,
distribution.installer_filepath
)
sys.exit(1)
# Use new executable to relaunch different AYON launcher version
executable = distribution.installer_executable
args = list(ORIGINAL_ARGS)
# Replace executable with new executable
args[0] = executable
# TODO figure out how this should be launched
# - it can technically cause infinite loop of subprocesses
sys.exit(subprocess.call(args))
# TODO check failed distribution and inform user
distribution.validate_distribution()
os.environ["AYON_BUNDLE_NAME"] = bundle_name
# TODO probably remove paths to other addons?
python_paths = [
path
for path in os.getenv("PYTHONPATH", "").split(os.pathsep)
if path
]
for path in distribution.get_python_paths():
sys.path.insert(0, path)
if path not in python_paths:
python_paths.append(path)
for path in distribution.get_sys_paths():
sys.path.insert(0, path)
os.environ["PYTHONPATH"] = os.pathsep.join(python_paths)
def boot():
"""Bootstrap AYON."""
# Setup site id in environment variable for all possible subprocesses
if SITE_ID_ENV_KEY not in os.environ:
os.environ[SITE_ID_ENV_KEY] = get_local_site_id()
_connect_to_ayon_server()
create_global_connection()
_start_distribution()
store_current_executable_info()
def _on_main_addon_missing():
if HEADLESS_MODE_ENABLED:
raise RuntimeError("Failed to import required AYON core addon.")
show_startup_error(
"Missing core addon",
(
"AYON-launcher requires AYON core addon to be able to start."
"<br/><br/>Please contact your administrator"
" to resolve the issue."
)
)
sys.exit(1)
def _on_main_addon_import_error(exception):
if HEADLESS_MODE_ENABLED:
raise RuntimeError(
"Failed to import AYON core addon. Probably because"
" of missing or incompatible dependency package"
)
show_startup_error(
"Incompatible Dependency package",
(
"Dependency package is missing or incompatible with available"
" addons."
"<br/><br/>Please contact your administrator"
" to resolve the issue."
),
str(exception)
)
sys.exit(1)
def _main_cli_openpype():
try:
from openpype import PACKAGE_DIR
except ImportError:
_on_main_addon_missing()
try:
from openpype import cli
except ImportError as exc:
traceback.print_exception(*sys.exc_info())
_on_main_addon_import_error(exc)
python_path = os.getenv("PYTHONPATH", "")
split_paths = python_path.split(os.pathsep)
# TODO move to ayon core import
additional_paths = [
# add OpenPype tools
os.path.join(PACKAGE_DIR, "tools"),
# add common OpenPype vendor
# (common for multiple Python interpreter versions)
os.path.join(PACKAGE_DIR, "vendor", "python", "common")
]
for path in additional_paths:
if path not in split_paths:
split_paths.insert(0, path)
if path not in sys.path:
sys.path.insert(0, path)
os.environ["PYTHONPATH"] = os.pathsep.join(split_paths)
_print(">>> loading environments ...")
_print(" - global AYON ...")
set_global_environments()
_print(" - for addons ...")
set_addons_environments()
# print info when not running scripts defined in 'silent commands'
if not SKIP_HEADERS:
info = get_info(is_staging_enabled(), is_dev_mode_enabled())
info.insert(0, f">>> Using AYON from [ {AYON_ROOT} ]")
t_width = 20
with contextlib.suppress(ValueError, OSError):
t_width = os.get_terminal_size().columns - 2
_header = f"*** AYON [{__version__}] "
info.insert(0, _header + "-" * (t_width - len(_header)))
for i in info:
_print(i)
try:
cli.main(obj={}, prog_name="ayon")
except Exception: # noqa
exc_info = sys.exc_info()
_print("!!! AYON crashed:")
traceback.print_exception(*exc_info)
sys.exit(1)
def main_cli():
"""Main startup logic.
This is the main entry point for the AYON launcher. At this
moment is fully dependent on 'ayon_core' addon. Which means it
contains more logic than it should.
"""
try:
import ayon_core
ayon_core_used = True
except ImportError:
ayon_core_used = False
if not ayon_core_used:
return _main_cli_openpype()
try:
from ayon_core import cli
except ImportError as exc:
traceback.print_exception(*sys.exc_info())
_on_main_addon_import_error(exc)
# print info when not running scripts defined in 'silent commands'
if not SKIP_HEADERS:
info = get_info(is_staging_enabled(), is_dev_mode_enabled())
info.insert(0, f">>> Using AYON from [ {AYON_ROOT} ]")
t_width = 20
with contextlib.suppress(ValueError, OSError):
t_width = os.get_terminal_size().columns - 2
_header = f"*** AYON [{__version__}] "
info.insert(0, _header + "-" * (t_width - len(_header)))
for i in info:
_print(i)
try:
cli.main()
except Exception: # noqa
exc_info = sys.exc_info()
_print("!!! AYON crashed:")
traceback.print_exception(*exc_info)
sys.exit(1)
class StartArgScript:
def __init__(self, argument, script_path):
self.argument = argument
self.script_path = script_path
@property
def is_valid(self):
return self.script_path is not None
@property
def is_dir(self):
if self.argument:
return os.path.isdir(self.argument)
return False
@classmethod
def from_args(cls, args):
"""Get path argument from args and check if can be started.
Args:
args (Iterable[str]): Arguments passed to AYON.
Returns:
StartArgScript: Object containing argument and script path.
"""
if len(args) < 2:
return cls(None, None)
path = args[1]
if os.path.exists(path):
if os.path.isdir(path):
new_path = os.path.join(path, "__main__.py")
if os.path.exists(new_path):
return cls(path, new_path)
else:
path_ext = os.path.splitext(path)[1].lower()
if path_ext in (".py", ".pyd", ".pyw", ".pyc"):
return cls(path, path)
return cls(path, None)
def script_cli(start_arg=None):
"""Run and execute script."""
if start_arg is None:
start_arg = StartArgScript.from_args(sys.argv)
# Remove first argument from sys.argv
# - start.py when running from code
# - ayon executable when running from build
sys.argv.pop(0)
# Find '__main__.py' in directory
if not start_arg.is_valid:
if not start_arg.argument:
raise RuntimeError("No script to run")
if start_arg.is_dir:
raise RuntimeError(
f"Can't find '__main__' module in '{start_arg.argument}'")
raise RuntimeError(f"Can't find script to run '{start_arg.argument}'")
filepath = start_arg.script_path
# Add parent dir to sys path
sys.path.insert(0, os.path.dirname(filepath))
# Read content and execute
with open(filepath, "r") as stream:
content = stream.read()
script_globals = dict(globals())
script_globals["__file__"] = filepath
exec(compile(content, filepath, "exec"), script_globals)
def get_info(use_staging=None, use_dev=None) -> list:
"""Print additional information to console."""
inf = []
bundle_name = os.getenv("AYON_BUNDLE_NAME")
variant = "production"
if use_dev:
variant = "dev ({})".format(bundle_name)
elif use_staging:
variant = "staging"
inf.append(("AYON variant", variant))
inf.append(("AYON bundle", bundle_name))
# NOTE add addons information
maximum = max(len(i[0]) for i in inf)
formatted = []
for info in inf:
padding = (maximum - len(info[0])) + 1
formatted.append(f'... {info[0]}:{" " * padding}[ {info[1]} ]')
return formatted
def main():
if SHOW_LOGIN_UI:
if HEADLESS_MODE_ENABLED:
_print((
"!!! Invalid arguments combination"
" '--ayon-login' and '--headless'."
))
sys.exit(1)
_connect_to_ayon_server(True)
if SKIP_BOOTSTRAP:
return script_cli()
boot()
start_arg = StartArgScript.from_args(sys.argv)
if start_arg.is_valid:
script_cli(start_arg)
else:
main_cli()
if __name__ == "__main__":
main()