Repository: ynput/OpenPype
Issue: OpenPype specific version issues

## Description
Arguments `--use-version` and `--use-staging` work for just executed OpenPype process but it is not possible to execute children processes with same version.

### When it's problem
- ftrack event server (in both tray and studio service)
- all scripts executed from host (e.g. burnin script)
- all hosts using python (OpenPype) process before running real application (Harmony, Photoshop, AfterEffects and TVPaint)
- ...


Changed files:
igniter/bootstrap_repos.py, start.py

diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py
index b44689ba89..7c4f8b4b69 100644
--- a/igniter/bootstrap_repos.py
+++ b/igniter/bootstrap_repos.py
@@ -203,6 +203,12 @@ class OpenPypeVersion(semver.VersionInfo):
             openpype_version.staging = True
         return openpype_version
 
+    def __hash__(self):
+        if self.path:
+            return hash(self.path)
+        else:
+            return hash(str(self))
+
 
 class BootstrapRepos:
     """Class for bootstrapping local OpenPype installation.
@@ -650,6 +656,9 @@ class BootstrapRepos:
                 v for v in openpype_versions if v.path.suffix != ".zip"
             ]
 
+        # remove duplicates
+        openpype_versions = list(set(openpype_versions))
+
         return openpype_versions
 
     def process_entered_location(self, location: str) -> Union[Path, None]:
diff --git a/start.py b/start.py
index 660d0c9006..8ee9775ce8 100644
--- a/start.py
+++ b/start.py
@@ -6,10 +6,11 @@ Bootstrapping process of OpenPype is as follows:
 `OPENPYPE_PATH` is checked for existence - either one from environment or
 from user settings. Precedence takes the one set by environment.
 
-On this path we try to find OpenPype in directories version string in their names.
-For example: `openpype-v3.0.1-foo` is valid name, or even `foo_3.0.2` - as long
-as version can be determined from its name _AND_ file `openpype/openpype/version.py`
-can be found inside, it is considered OpenPype installation.
+On this path we try to find OpenPype in directories version string in their
+names. For example: `openpype-v3.0.1-foo` is valid name, or
+even `foo_3.0.2` - as long as version can be determined from its name
+_AND_ file `openpype/openpype/version.py` can be found inside, it is
+considered OpenPype installation.
 
 If no OpenPype repositories are found in `OPENPYPE_PATH` (user data dir)
 then **Igniter** (OpenPype setup tool) will launch its GUI.
@@ -20,19 +21,19 @@ appdata dir in user home and extract it there. Version will be determined by
 version specified in OpenPype module.
 
 If OpenPype repository directories are found in default install location
-(user data dir) or in `OPENPYPE_PATH`, it will get list of those dirs there and
-use latest one or the one specified with optional `--use-version` command
-line argument. If the one specified doesn't exist then latest available
-version will be used. All repositories in that dir will be added
+(user data dir) or in `OPENPYPE_PATH`, it will get list of those dirs
+there and use latest one or the one specified with optional `--use-version`
+command line argument. If the one specified doesn't exist then latest
+available version will be used. All repositories in that dir will be added
 to `sys.path` and `PYTHONPATH`.
 
-If OpenPype is live (not frozen) then current version of OpenPype module will be
-used. All directories under `repos` will be added to `sys.path` and
+If OpenPype is live (not frozen) then current version of OpenPype module
+will be used. All directories under `repos` will be added to `sys.path` and
 `PYTHONPATH`.
 
-OpenPype depends on connection to `MongoDB`_. You can specify MongoDB connection
-string via `OPENPYPE_MONGO` set in environment or it can be set in user
-settings or via **Igniter** GUI.
+OpenPype depends on connection to `MongoDB`_. You can specify MongoDB
+connection string via `OPENPYPE_MONGO` set in environment or it can be set
+in user settings or via **Igniter** GUI.
 
 So, bootstrapping OpenPype looks like this::
 
@@ -282,7 +283,8 @@ def _process_arguments() -> tuple:
             print("    --use-version=3.0.0")
             sys.exit(1)
 
-        m = re.search(r"--use-version=(?P<version>\\d+\\.\\d+\\.\\d*.+?)", arg)
+        m = re.search(
+            r"--use-version=(?P<version>\\d+\\.\\d+\\.\\d+(?:\\S*)?)", arg)
         if m and m.group('version'):
             use_version = m.group('version')
             sys.argv.remove(arg)
@@ -414,6 +416,7 @@ def _find_frozen_openpype(use_version: str = None,
             (if requested).
 
     """
+    version_path = None
     openpype_version = None
     openpype_versions = bootstrap.find_openpype(include_zips=True,
                                                 staging=use_staging)
@@ -433,7 +436,6 @@ def _find_frozen_openpype(use_version: str = None,
         if local_version == openpype_versions[-1]:
             os.environ["OPENPYPE_TRYOUT"] = "1"
             openpype_versions = []
-
     else:
         print("!!! Warning: cannot determine current running version.")
 
@@ -480,17 +482,25 @@ def _find_frozen_openpype(use_version: str = None,
         return version_path
 
     # get path of version specified in `--use-version`
-    version_path = BootstrapRepos.get_version_path_from_list(
-        use_version, openpype_versions)
-
-    if not version_path:
-        if use_version is not None and openpype_version:
-            print(("!!! Specified version was not found, using "
-                   "latest available"))
-        # specified version was not found so use latest detected.
-        version_path = openpype_version.path
-        print(f">>> Using version [ {openpype_version} ]")
-        print(f"    From {version_path}")
+    local_version = bootstrap.get_version(OPENPYPE_ROOT)
+    if use_version and use_version != local_version:
+        # force the one user has selected
+        openpype_version = None
+        openpype_versions = bootstrap.find_openpype(include_zips=True,
+                                                    staging=use_staging)
+        v: OpenPypeVersion
+        found = [v for v in openpype_versions if str(v) == use_version]
+        if found:
+            openpype_version = sorted(found)[-1]
+        if not openpype_version:
+            print(f"!!! requested version {use_version} was not found.")
+            if openpype_versions:
+                print("  - found: ")
+                for v in sorted(openpype_versions):
+                    print(f"     - {v}: {v.path}")
+
+            print(f"     - local version {local_version}")
+            sys.exit(1)
 
     # test if latest detected is installed (in user data dir)
     is_inside = False
@@ -521,7 +531,7 @@ def _find_frozen_openpype(use_version: str = None,
         openpype_version.path = version_path
 
     _initialize_environment(openpype_version)
-    return version_path
+    return openpype_version.path
 
 
 def _bootstrap_from_code(use_version):
@@ -536,36 +546,53 @@ def _bootstrap_from_code(use_version):
     """
     # run through repos and add them to `sys.path` and `PYTHONPATH`
     # set root
+    _openpype_root = OPENPYPE_ROOT
     if getattr(sys, 'frozen', False):
-        local_version = bootstrap.get_version(Path(OPENPYPE_ROOT))
+        local_version = bootstrap.get_version(Path(_openpype_root))
         print(f"  - running version: {local_version}")
         assert local_version
     else:
         # get current version of OpenPype
         local_version = bootstrap.get_local_live_version()
 
-    os.environ["OPENPYPE_VERSION"] = local_version
     if use_version and use_version != local_version:
+        version_to_use = None
         openpype_versions = bootstrap.find_openpype(include_zips=True)
-        version_path = BootstrapRepos.get_version_path_from_list(
-            use_version, openpype_versions)
-        if version_path:
+        v: OpenPypeVersion
+        found = [v for v in openpype_versions if str(v) == use_version]
+        if found:
+            version_to_use = sorted(found)[-1]
+
+        if version_to_use:
             # use specified
-            bootstrap.add_paths_from_directory(version_path)
+            if version_to_use.path.is_file():
+                version_to_use.path = bootstrap.extract_openpype(
+                    version_to_use)
+            bootstrap.add_paths_from_directory(version_to_use.path)
             os.environ["OPENPYPE_VERSION"] = use_version
+            version_path = version_to_use.path
+            os.environ["OPENPYPE_REPOS_ROOT"] = (version_path / "openpype").as_posix()  # noqa: E501
+            _openpype_root = version_to_use.path.as_posix()
+        else:
+            print(f"!!! requested version {use_version} was not found.")
+            if openpype_versions:
+                print("  - found: ")
+                for v in sorted(openpype_versions):
+                    print(f"     - {v}: {v.path}")
+
+            print(f"     - local version {local_version}")
+            sys.exit(1)
     else:
-        version_path = OPENPYPE_ROOT
+        os.environ["OPENPYPE_VERSION"] = local_version
+        version_path = Path(_openpype_root)
+        os.environ["OPENPYPE_REPOS_ROOT"] = _openpype_root
 
-    repos = os.listdir(os.path.join(OPENPYPE_ROOT, "repos"))
-    repos = [os.path.join(OPENPYPE_ROOT, "repos", repo) for repo in repos]
+    repos = os.listdir(os.path.join(_openpype_root, "repos"))
+    repos = [os.path.join(_openpype_root, "repos", repo) for repo in repos]
     # add self to python paths
-    repos.insert(0, OPENPYPE_ROOT)
+    repos.insert(0, _openpype_root)
     for repo in repos:
         sys.path.insert(0, repo)
-
-    # Set OPENPYPE_REPOS_ROOT to code root
-    os.environ["OPENPYPE_REPOS_ROOT"] = OPENPYPE_ROOT
-
     # add venv 'site-packages' to PYTHONPATH
     python_path = os.getenv("PYTHONPATH", "")
     split_paths = python_path.split(os.pathsep)
@@ -580,11 +607,11 @@ def _bootstrap_from_code(use_version):
         # point to same hierarchy from code and from frozen OpenPype
         additional_paths = [
             # add OpenPype tools
-            os.path.join(OPENPYPE_ROOT, "openpype", "tools"),
+            os.path.join(_openpype_root, "openpype", "tools"),
             # add common OpenPype vendor
             # (common for multiple Python interpreter versions)
             os.path.join(
-                OPENPYPE_ROOT,
+                _openpype_root,
                 "openpype",
                 "vendor",
                 "python",
@@ -597,7 +624,7 @@ def _bootstrap_from_code(use_version):
 
     os.environ["PYTHONPATH"] = os.pathsep.join(split_paths)
 
-    return Path(version_path)
+    return version_path
 
 
 def boot():
@@ -624,6 +651,10 @@ def boot():
 
     use_version, use_staging = _process_arguments()
 
+    if os.getenv("OPENPYPE_VERSION"):
+        use_staging = "staging" in os.getenv("OPENPYPE_VERSION")
+        use_version = os.getenv("OPENPYPE_VERSION")
+
     # ------------------------------------------------------------------------
     # Determine mongodb connection
     # ------------------------------------------------------------------------