You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@buildstream.apache.org by tv...@apache.org on 2021/01/14 07:31:08 UTC

[buildstream] 01/08: Allow certain operations to work without loading a project

This is an automated email from the ASF dual-hosted git repository.

tvb pushed a commit to branch tristan/optional-project
in repository https://gitbox.apache.org/repos/asf/buildstream.git

commit 8f0b85ac77a8e0aedc99fade4b125cf5dc1ca9f5
Author: Tristan van Berkom <tr...@codethink.co.uk>
AuthorDate: Thu Nov 19 16:35:33 2020 +0900

    Allow certain operations to work without loading a project
    
    This patch removes the requirement to load a project.conf at startup time,
    accomodating certain operations which do not need to observe workspace
    state or load elements.
    
    Summary of changes:
    
      * _frontend/app.py: Don't error out if a project cannot be loaded.
    
        The project is optional, and Stream() will raise an error if a project
        is required for the operation it is asked to perform.
    
      * _frontend/widget.py: Support printing the heading without a toplevel
        project.
    
      * _stream.py: Handle cases where the main project is None, and
        raise an error in cases where a project is required.
    
      * _workspaces.py: Handle cases where the project directory is None,
        in case we are looking at an ArtifactProject which has no directory.
    
      * tests/format/project.py: Update the missing project test to expect
        a different error, and observe multiple cases where a project might
        be required.
    
        We still have an error for missing projects, but it is only issued
        for cases where a project is expected, we need to continue testing this.
---
 src/buildstream/_frontend/app.py    | 23 ++++++------
 src/buildstream/_frontend/widget.py | 12 +++++--
 src/buildstream/_stream.py          | 72 +++++++++++++++++++++++++++----------
 src/buildstream/_workspaces.py      | 10 ++++--
 tests/format/project.py             |  7 ++--
 5 files changed, 86 insertions(+), 38 deletions(-)

diff --git a/src/buildstream/_frontend/app.py b/src/buildstream/_frontend/app.py
index 9937470..ce1610f 100644
--- a/src/buildstream/_frontend/app.py
+++ b/src/buildstream/_frontend/app.py
@@ -284,23 +284,24 @@ class App:
                     cli_options=self._main_options["option"],
                     default_mirror=self._main_options.get("default_mirror"),
                 )
-
-                self.stream.set_project(self.project)
             except LoadError as e:
 
-                # Help users that are new to BuildStream by suggesting 'init'.
-                # We don't want to slow down users that just made a mistake, so
-                # don't stop them with an offer to create a project for them.
-                if e.reason == LoadErrorReason.MISSING_PROJECT_CONF:
-                    click.echo("No project found. You can create a new project like so:", err=True)
-                    click.echo("", err=True)
-                    click.echo("    bst init", err=True)
-
-                self._error_exit(e, "Error loading project")
+                # If there was no project.conf at all then there was just no project found.
+                #
+                # Don't error out in this case, as Stream() supports some operations which
+                # do not require a project. If Stream() requires a project and it is missing,
+                # then it will raise an error.
+                #
+                if e.reason != LoadErrorReason.MISSING_PROJECT_CONF:
+                    self._error_exit(e, "Error loading project")
 
             except BstError as e:
                 self._error_exit(e, "Error loading project")
 
+            # Set the project on the Stream, this can be None if there is no project.
+            #
+            self.stream.set_project(self.project)
+
             # Run the body of the session here, once everything is loaded
             try:
                 yield
diff --git a/src/buildstream/_frontend/widget.py b/src/buildstream/_frontend/widget.py
index 0d5379f..dfc340a 100644
--- a/src/buildstream/_frontend/widget.py
+++ b/src/buildstream/_frontend/widget.py
@@ -444,7 +444,7 @@ class LogLine(Widget):
     # and so on.
     #
     # Args:
-    #    toplevel_project (Project): The toplevel project we were invoked from
+    #    toplevel_project (Project): The toplevel project we were invoked from, or None
     #    stream (Stream): The stream
     #    log_file (file): An optional file handle for additional logging
     #
@@ -460,7 +460,8 @@ class LogLine(Widget):
         text += self.content_profile.fmt("BuildStream Version {}\n".format(bst_version), bold=True)
         values = OrderedDict()
         values["Session Start"] = starttime.strftime("%A, %d-%m-%Y at %H:%M:%S")
-        values["Project"] = "{} ({})".format(toplevel_project.name, toplevel_project.directory)
+        if toplevel_project:
+            values["Project"] = "{} ({})".format(toplevel_project.name, toplevel_project.directory)
         values["Targets"] = ", ".join([t.name for t in stream.targets])
         text += self._format_values(values)
 
@@ -483,7 +484,12 @@ class LogLine(Widget):
 
         # Print information about each loaded project
         #
-        for project_info in toplevel_project.loaded_projects():
+        if toplevel_project:
+            loaded_projects = toplevel_project.loaded_projects()
+        else:
+            loaded_projects = []
+
+        for project_info in loaded_projects:
             project = project_info.project
 
             # Project title line
diff --git a/src/buildstream/_stream.py b/src/buildstream/_stream.py
index 22feab9..6eb25e8 100644
--- a/src/buildstream/_stream.py
+++ b/src/buildstream/_stream.py
@@ -124,7 +124,8 @@ class Stream:
     def set_project(self, project):
         assert self._project is None
         self._project = project
-        self._project.load_context.set_fetch_subprojects(self._fetch_subprojects)
+        if self._project:
+            self._project.load_context.set_fetch_subprojects(self._fetch_subprojects)
 
     # load_selection()
     #
@@ -885,6 +886,8 @@ class Stream:
     #    remove_dir (bool): Whether to remove the associated directory
     #
     def workspace_close(self, element_name, *, remove_dir):
+        self._assert_project("Unable to locate workspaces")
+
         workspaces = self._context.get_workspaces()
         workspace = workspaces.get_workspace(element_name)
 
@@ -913,6 +916,7 @@ class Stream:
     #    soft (bool): Only set the workspace state to not prepared
     #
     def workspace_reset(self, targets, *, soft):
+        self._assert_project("Unable to locate workspaces")
 
         elements = self._load(targets, selection=_PipelineSelection.REDIRECT)
 
@@ -953,6 +957,8 @@ class Stream:
     # True if there are any existing workspaces.
     #
     def workspace_exists(self, element_name=None):
+        self._assert_project("Unable to locate workspaces")
+
         workspaces = self._context.get_workspaces()
         if element_name:
             workspace = workspaces.get_workspace(element_name)
@@ -968,6 +974,8 @@ class Stream:
     # Serializes the workspaces and dumps them in YAML to stdout.
     #
     def workspace_list(self):
+        self._assert_project("Unable to locate workspaces")
+
         workspaces = []
         for element_name, workspace_ in self._context.get_workspaces().list():
             workspace_detail = {
@@ -1106,6 +1114,22 @@ class Stream:
     #                    Private Methods                        #
     #############################################################
 
+    # _assert_project()
+    #
+    # Raises an assertion of a project was not loaded
+    #
+    # Args:
+    #    message: The user facing error message, e.g. "Unable to load elements"
+    #
+    # Raises:
+    #    A StreamError with reason "project-not-loaded" is raised if no project was loaded
+    #
+    def _assert_project(self, message: str) -> None:
+        if not self._project:
+            raise StreamError(
+                message, detail="No project.conf or active workspace was located", reason="project-not-loaded"
+            )
+
     # _fetch_subprojects()
     #
     # Fetch subprojects as part of the project and element loading process.
@@ -1210,7 +1234,12 @@ class Stream:
             targets, valid_artifact_names=valid_artifact_names
         )
 
-        self._project.load_context.set_rewritable(rewritable)
+        # We need a project in order to load elements
+        if element_names:
+            self._assert_project("Unable to load elements: {}".format(", ".join(element_names)))
+
+        if self._project:
+            self._project.load_context.set_rewritable(rewritable)
 
         # Load elements and except elements
         if element_names:
@@ -1493,7 +1522,11 @@ class Stream:
     def _resolve_elements(self, targets):
         with self._context.messenger.simple_task("Resolving cached state", silent_nested=True) as task:
             # We need to go through the project to access the loader
-            if task:
+            #
+            # FIXME: We need to calculate the total elements to resolve differently so that
+            #        it can include artifact elements
+            #
+            if task and self._project:
                 task.set_maximum_progress(self._project.loader.loaded)
 
             # XXX: Now that Element._update_state() can trigger recursive update_state calls
@@ -1845,22 +1878,23 @@ class Stream:
             element_targets = initial_targets
 
         # Expand globs for elements
-        all_elements = []
-        element_path_length = len(self._project.element_path) + 1
-        for dirpath, _, filenames in os.walk(self._project.element_path):
-            for filename in filenames:
-                if filename.endswith(".bst"):
-                    element_path = os.path.join(dirpath, filename)
-                    element_path = element_path[element_path_length:]  # Strip out the element_path
-                    all_elements.append(element_path)
-
-        for glob in globs:
-            matched = False
-            for element_path in utils.glob(all_elements, glob):
-                element_targets.append(element_path)
-                matched = True
-            if matched:
-                globs[glob] = globs[glob] + 1
+        if self._project:
+            all_elements = []
+            element_path_length = len(self._project.element_path) + 1
+            for dirpath, _, filenames in os.walk(self._project.element_path):
+                for filename in filenames:
+                    if filename.endswith(".bst"):
+                        element_path = os.path.join(dirpath, filename)
+                        element_path = element_path[element_path_length:]  # Strip out the element_path
+                        all_elements.append(element_path)
+
+            for glob in globs:
+                matched = False
+                for element_path in utils.glob(all_elements, glob):
+                    element_targets.append(element_path)
+                    matched = True
+                if matched:
+                    globs[glob] = globs[glob] + 1
 
         # Expand globs for artifact names
         if valid_artifact_names:
diff --git a/src/buildstream/_workspaces.py b/src/buildstream/_workspaces.py
index e51be08..ebca148 100644
--- a/src/buildstream/_workspaces.py
+++ b/src/buildstream/_workspaces.py
@@ -313,10 +313,16 @@ class Workspace:
 class Workspaces:
     def __init__(self, toplevel_project, workspace_project_cache):
         self._toplevel_project = toplevel_project
-        self._bst_directory = os.path.join(toplevel_project.directory, ".bst")
-        self._workspaces = self._load_config()
         self._workspace_project_cache = workspace_project_cache
 
+        # A project without a directory can happen
+        if toplevel_project.directory:
+            self._bst_directory = os.path.join(toplevel_project.directory, ".bst")
+            self._workspaces = self._load_config()
+        else:
+            self._bst_directory = None
+            self._workspaces = {}
+
     # list()
     #
     # Generator function to enumerate workspaces.
diff --git a/tests/format/project.py b/tests/format/project.py
index d3de672..6e06176 100644
--- a/tests/format/project.py
+++ b/tests/format/project.py
@@ -15,10 +15,11 @@ DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "project")
 
 
 @pytest.mark.datafiles(os.path.join(DATA_DIR))
-def test_missing_project_conf(cli, datafiles):
+@pytest.mark.parametrize("args", [["workspace", "list"], ["show", "pony.bst"]], ids=["list-workspace", "show-element"])
+def test_missing_project_conf(cli, datafiles, args):
     project = str(datafiles)
-    result = cli.run(project=project, args=["workspace", "list"])
-    result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.MISSING_PROJECT_CONF)
+    result = cli.run(project=project, args=args)
+    result.assert_main_error(ErrorDomain.STREAM, "project-not-loaded")
 
 
 @pytest.mark.datafiles(os.path.join(DATA_DIR))