Repository: tianocore/edk2-pytool-extensions

Issue:
[Feature]: Allow extdep to use local resource for co-development
### What does the feature solve?

When using extdep for a co-developed component such as in a git or nuget extdep. Currently, you can overrite the location using a build environment variable, but this does not work for build plugins or other self-describing environment components. 

### Describe the solution

Add a option similar to "pip install -e" that allows an extdep to symlink to a local copy of the resource to support co-development. This should be saved into the state file so that the pre-build check also knows to ignore the version or dirty state checks. This could be done by running a standalone command or adding flag options to stuart_update.

### Have you considered any alternatives?

_No response_

### Additional context

This is part of an effort Project MU is trying to create more stand-alone feature repos that can be consumed not as a submodule but as a complete package either through nuget or a git extdep. This is used for the following repos.

https://github.com/microsoft/mu_feature_ipmi
https://github.com/microsoft/mu_feature_mm_supv
https://github.com/microsoft/mu_feature_config

Comments:

Patch:
diff --git a/edk2toolext/environment/extdeptypes/git_dependency.py b/edk2toolext/environment/extdeptypes/git_dependency.py
index 3b67974..be3e94d 100644
--- a/edk2toolext/environment/extdeptypes/git_dependency.py
+++ b/edk2toolext/environment/extdeptypes/git_dependency.py
@@ -83,7 +83,17 @@ def clean(self):
         super().clean()
 
     def verify(self):
-        """Verifies the clone was successful."""
+        """Verifies the clone was successful.
+
+        !!! Note
+            If verify is set to false in the dependencies state file,
+            it will always skip the verification process.
+        """
+        state_data = self.get_state_file_data()
+        if state_data and state_data['verify'] is False:
+            logging.warn(f'{self.name} is unverified. Unexpected results may occur.')
+            return True
+
         result = True
 
         if not os.path.isdir(self._local_repo_root_path):
diff --git a/edk2toolext/environment/external_dependency.py b/edk2toolext/environment/external_dependency.py
index ef2aec3..0a38bd9 100644
--- a/edk2toolext/environment/external_dependency.py
+++ b/edk2toolext/environment/external_dependency.py
@@ -65,7 +65,7 @@ def __init__(self, descriptor):
         self.contents_dir = os.path.join(
             self.descriptor_location, self.name + "_extdep")
         self.state_file_path = os.path.join(
-            self.contents_dir, "extdep_state.json")
+            self.contents_dir, "extdep_state.yaml")
         self.published_path = self.compute_published_path()
 
     def set_global_cache_path(self, global_cache_path):
@@ -177,19 +177,8 @@ def copy_to_global_cache(self, source_path: str):
     def verify(self):
         """Verifies the dependency was successfully downloaded."""
         result = True
-        state_data = None
+        state_data = self.get_state_file_data()
 
-        # See whether or not the state file exists.
-        if not os.path.isfile(self.state_file_path):
-            result = False
-
-        # Attempt to load the state file.
-        if result:
-            with open(self.state_file_path, 'r') as file:
-                try:
-                    state_data = yaml.safe_load(file)
-                except Exception:
-                    pass
         if state_data is None:
             result = False
 
@@ -202,15 +191,27 @@ def verify(self):
 
     def report_version(self):
         """Reports the version of the external dependency."""
+        state_data = self.get_state_file_data()
+        version = self.version
+        if state_data and state_data['verify'] is False:
+            version = "UNVERIFIED"
         version_aggregator.GetVersionAggregator().ReportVersion(self.name,
-                                                                self.version,
+                                                                version,
                                                                 version_aggregator.VersionTypes.INFO,
                                                                 self.descriptor_location)
 
     def update_state_file(self):
         """Updates the file representing the state of the dependency."""
         with open(self.state_file_path, 'w+') as file:
-            yaml.dump({'version': self.version}, file)
+            yaml.dump({'version': self.version, 'verify': True}, file)
+
+    def get_state_file_data(self):
+        """Loads the state file data into a json file and returns it."""
+        try:
+            with open(self.state_file_path, 'r') as file:
+                return yaml.safe_load(file)
+        except Exception:
+            return None
 
 
 def ExtDepFactory(descriptor):
diff --git a/edk2toolext/invocables/edk2_platform_build.py b/edk2toolext/invocables/edk2_platform_build.py
index 6bec14d..b24f2cd 100644
--- a/edk2toolext/invocables/edk2_platform_build.py
+++ b/edk2toolext/invocables/edk2_platform_build.py
@@ -68,12 +68,29 @@ def AddCommandLineOptions(self, parserObj):
             except (TypeError):
                 raise RuntimeError(f"UefiBuild not found in module:\n{dir(self.PlatformModule)}")
 
+        parserObj.add_argument('-nv', '-NV', '--noverify', '--NOVERIFY', '--NoVerify',
+                               dest="verify", default=True, action='store_false',
+                               help='Skip verifying external dependencies before build.')
         self.PlatformBuilder.AddPlatformCommandLineOptions(parserObj)
 
     def RetrieveCommandLineOptions(self, args):
         """Retrieve command line options from the argparser."""
+        self.verify = args.verify
         self.PlatformBuilder.RetrievePlatformCommandLineOptions(args)
 
+    def GetVerifyCheckRequired(self) -> bool:
+        """Will call self_describing_environment.VerifyEnvironment if this returns True.
+
+        !!! hint
+            Optional override in a subclass
+
+        Returns:
+            (bool): whether verify check is required or not
+        """
+        if not self.verify:
+            logging.warning("Skipping Environment Verification. Unexpected results may occur.")
+        return self.verify
+
     def GetSettingsClass(self):
         """Returns the BuildSettingsManager class.
 


Test patch:
diff --git a/edk2toolext/tests/test_edk2_update.py b/edk2toolext/tests/test_edk2_update.py
index c712e86..25ee5b5 100644
--- a/edk2toolext/tests/test_edk2_update.py
+++ b/edk2toolext/tests/test_edk2_update.py
@@ -84,7 +84,7 @@ def test_one_level_recursive(self):
         updater = self.invoke_update(tree.get_settings_provider_path())
         # make sure it worked
         self.assertTrue(os.path.exists(os.path.join(WORKSPACE, "Edk2TestUpdate_extdep",
-                                                    "NuGet.CommandLine_extdep", "extdep_state.json")))
+                                                    "NuGet.CommandLine_extdep", "extdep_state.yaml")))
         build_env, shell_env, failure = updater.PerformUpdate()
         # we should have no failures
         self.assertEqual(failure, 0)
diff --git a/edk2toolext/tests/test_self_describing_environment.py b/edk2toolext/tests/test_self_describing_environment.py
index 2277791..3fcf43d 100644
--- a/edk2toolext/tests/test_self_describing_environment.py
+++ b/edk2toolext/tests/test_self_describing_environment.py
@@ -10,6 +10,7 @@
 import pygit2
 import unittest
 import tempfile
+import yaml
 from edk2toolext.environment import self_describing_environment
 from edk2toolext.tests.uefi_tree import uefi_tree
 from edk2toolext.environment import version_aggregator
@@ -166,6 +167,33 @@ def test_git_worktree(self):
         # that are present in the worktree
         self_describing_environment.BootstrapEnvironment(self.workspace, ('global',))
 
+    def test_no_verify_extdep(self):
+        tree = uefi_tree(self.workspace, create_platform=False)
+        tree.create_ext_dep(dep_type="git",
+                            scope="global",
+                            name="HelloWorld",
+                            source="https://github.com/octocat/Hello-World.git",
+                            version="7fd1a60b01f91b314f59955a4e4d4e80d8edf11d")
+
+        # Bootstrap the environment
+        self_describing_environment.BootstrapEnvironment(self.workspace, ("global",))
+        self_describing_environment.UpdateDependencies(self.workspace, scopes=("global",))
+        self_describing_environment.VerifyEnvironment(self.workspace, scopes=("global",))
+
+        # Delete the readme to make the repo dirty then verify it fails
+        readme = os.path.join(tree.get_workspace(), "HelloWorld_extdep", "HelloWorld", "README")
+        os.remove(readme)
+        self.assertFalse(self_describing_environment.VerifyEnvironment(self.workspace, scopes=("global",)))
+
+        # Update the state file to not verify the specific external dependency then verify it passes
+        state_file = os.path.join(tree.get_workspace(), "HelloWorld_extdep", "extdep_state.yaml")
+        with open(state_file, 'r+') as f:
+            content = yaml.safe_load(f)
+            f.seek(0)
+            content["verify"] = False
+            yaml.safe_dump(content, f)
+        self.assertTrue(self_describing_environment.VerifyEnvironment(self.workspace, scopes=("global",)))
+
 
 if __name__ == '__main__':
     unittest.main()