[script/dvsim] Separate publish report from dvsim flow [PART2]

Add a `latest` directory under `reports` to symlink the latest result.
This will make it easier for other script to send emails and publish the
results.

Signed-off-by: Cindy Chen <chencindy@opentitan.org>
diff --git a/util/dvsim/FlowCfg.py b/util/dvsim/FlowCfg.py
index 809f0bb..ccf7a74 100644
--- a/util/dvsim/FlowCfg.py
+++ b/util/dvsim/FlowCfg.py
@@ -15,7 +15,7 @@
 from LauncherFactory import get_launcher_cls
 from Scheduler import Scheduler
 from utils import (VERBOSE, clean_odirs, find_and_substitute_wildcards, md_results_to_html,
-                   mk_path, rm_path, subst_wildcards)
+                   mk_path, mk_symlink, rm_path, subst_wildcards)
 
 
 # Interface class for extensions.
@@ -134,6 +134,8 @@
 
         self.results_path = os.path.join(self.scratch_base_path, "reports", self.rel_path,
                                          self.timestamp)
+        self.symlink_path = os.path.join(self.scratch_base_path, "reports", self.rel_path,
+                                         "latest")
 
         # Process overrides before substituting wildcards
         self._process_overrides()
@@ -422,19 +424,23 @@
         # Keep up to 2 weeks results.
         clean_odirs(odir=self.results_path, max_odirs=14)
         mk_path(self.results_path)
+        mk_path(self.symlink_path)
 
         # Write results to the report area.
         if self.is_primary_cfg:
             results_html = md_results_to_html(self.results_title, self.css_file,
                                               self.results_summary_md)
             result_path = os.path.join(self.results_path, "summary.html")
+            symlink_path = os.path.join(self.symlink_path, "summary.html")
         else:
             results_html = md_results_to_html(self.results_title, self.css_file,
                                               self.results_md)
             result_path = os.path.join(self.results_path, "results.html")
+            symlink_path = os.path.join(self.symlink_path, "results.html")
         with open(result_path, "w") as results_file:
             results_file.write(results_html)
         log.log(VERBOSE, "[results page]: [%s][%s], self.name, results_path")
+        mk_symlink(result_path, symlink_path)
 
     def _get_results_page_link(self, link_text):
         if not self.args.publish:
diff --git a/util/dvsim/Launcher.py b/util/dvsim/Launcher.py
index 5580ebe..cddf436 100644
--- a/util/dvsim/Launcher.py
+++ b/util/dvsim/Launcher.py
@@ -9,7 +9,7 @@
 import sys
 from pathlib import Path
 
-from utils import VERBOSE, clean_odirs, rm_path
+from utils import VERBOSE, clean_odirs, mk_symlink, rm_path
 
 
 class LauncherError(Exception):
@@ -161,13 +161,7 @@
 
         dest = Path(self.deploy.sim_cfg.links[status], self.deploy.qual_name)
 
-        # If dest exists, then atomically remove it and link the odir again.
-        while True:
-            try:
-                os.symlink(self.deploy.odir, dest)
-                break
-            except FileExistsError:
-                rm_path(dest)
+        mk_symlink(self.deploy.odir, dest)
 
         # Delete the symlink from dispatched directory if it exists.
         if status != "D":
diff --git a/util/dvsim/utils.py b/util/dvsim/utils.py
index 4916a8d..db539f6 100644
--- a/util/dvsim/utils.py
+++ b/util/dvsim/utils.py
@@ -569,6 +569,21 @@
         sys.exit(1)
 
 
+def mk_symlink(path, link):
+    '''Create a symlink from the given path.
+
+    'link' is a Path-like object. If it does exist, remove the existing link and
+    create a new symlink with this given path.
+    If it does not exist, the function creates the symlink with the given path.
+    '''
+    while True:
+        try:
+            os.symlink(path, link)
+            break
+        except FileExistsError:
+            rm_path(link)
+
+
 def clean_odirs(odir, max_odirs, ts_format=TS_FORMAT):
     """Clean previous output directories.