Skip to content

Commit

Permalink
pw_env_setup: Bootstrap fish-shell support
Browse files Browse the repository at this point in the history
Change-Id: I6d9838b0010ee34c8e50571439f71ecba00cfc3b
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/56840
Reviewed-by: Chad Norvell <[email protected]>
Reviewed-by: Rob Mohr <[email protected]>
Lint: Lint 🤖 <[email protected]>
Commit-Queue: Anthony DiGirolamo <[email protected]>
  • Loading branch information
AnthonyDiGirolamo authored and CQ Bot Account committed Jun 4, 2024
1 parent d910032 commit d988f70
Show file tree
Hide file tree
Showing 10 changed files with 673 additions and 14 deletions.
1 change: 1 addition & 0 deletions activate.fish
90 changes: 90 additions & 0 deletions bootstrap.fish
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Copyright 2024 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.

# This script must be tested on fish 3.6.0

# Get the absolute path of the bootstrap script.
set _PW_BOOTSTRAP_PATH (path resolve (status current-filename))

# Check if this file is being executed or sourced.
set _pw_sourced 0
if string match --quiet '*from sourcing file*' (status)
set _pw_sourced 1
end

# Downstream projects need to set something other than PW_ROOT here, like
# YOUR_PROJECT_ROOT. Please also set PW_PROJECT_ROOT and PW_ROOT before
# invoking pw_bootstrap or pw_activate.
######### BEGIN PROJECT-SPECIFIC CODE #########
set --export PW_ROOT (path resolve (dirname $_PW_BOOTSTRAP_PATH))

# Please also set PW_PROJECT_ROOT to YOUR_PROJECT_ROOT.
set --export PW_PROJECT_ROOT $PW_ROOT

# Downstream projects may wish to set PW_BANNER_FUNC to a function that prints
# an ASCII art banner here. For example:
#
# set --export PW_BANNER_FUNC banner_function_name
#
########## END PROJECT-SPECIFIC CODE ##########

source $PW_ROOT/pw_env_setup/util.fish

# Check environment properties
pw_deactivate
pw_eval_sourced $_pw_sourced $_PW_BOOTSTRAP_PATH
if not pw_check_root $PW_ROOT
return
end

set --export _PW_ACTUAL_ENVIRONMENT_ROOT (pw_get_env_root)

set SETUP_SH "$_PW_ACTUAL_ENVIRONMENT_ROOT/activate.fish"

# Run full bootstrap when invoked as bootstrap, or env file is missing/empty.
if test (status basename) = "bootstrap.fish"
or not test -e $SETUP_SH
or not test -s $SETUP_SH
######### BEGIN PROJECT-SPECIFIC CODE #########
pw_bootstrap --shell-file "$SETUP_SH" --install-dir "$_PW_ACTUAL_ENVIRONMENT_ROOT"
########## END PROJECT-SPECIFIC CODE ##########
set finalize_mode bootstrap
else
pw_activate_message
set finalize_mode activate
end
# NOTE: Sourced scripts in fish cannot be sourced within a fuction if
# variables should be exported to the calling shell. So activate.fish
# must be sourced here instead of within pw_finalize or another
# function.
pw_finalize_pre_check $finalize_mode "$SETUP_SH"
source $SETUP_SH
pw_finalize_post_check $finalize_mode "$SETUP_SH"

if set --query _PW_ENV_SETUP_STATUS; and test "$_PW_ENV_SETUP_STATUS" -eq 0
# This is where any additional checks about the environment should go.
######### BEGIN PROJECT-SPECIFIC CODE #########
echo -n
########## END PROJECT-SPECIFIC CODE ##########
end

set -e _pw_sourced
set -e _PW_BOOTSTRAP_PATH
set -e SETUP_SH

# TODO(tonymd): Source fish pw_cli shell completion.

pw_cleanup

git -C "$PW_ROOT" config blame.ignoreRevsFile .git-blame-ignore-revs
5 changes: 5 additions & 0 deletions docs/get_started/upstream.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ To get setup:
$ bootstrap.bat (On Windows)
...
.. tip::

If you use the `Fish shell <https://fishshell.com/>`_ run `source
./bootstrap.fish` instead.

#. Configure the GN build.

.. code-block:: bash
Expand Down
9 changes: 7 additions & 2 deletions pw_env_setup/docs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,13 @@ runs bootstrap.
.. note::

On Windows the scripts used to set up the environment are ``bootstrap.bat``
and ``activate.bat``. For simplicity they will be referred to with the
``.sh`` endings unless the distinction is relevant.
and ``activate.bat``.

``bootstrap.fish`` and ``activate.fish`` are also available for `Fish shell
<https://fishshell.com/>`_ users.

For simplicity they will be referred to with the ``.sh`` endings unless the
distinction is relevant.

On POSIX systems, the environment can be deactivated by running ``deactivate``.

Expand Down
6 changes: 3 additions & 3 deletions pw_env_setup/py/environment_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ def _evaluate_env_in_shell(env):
delete=False,
mode='w+',
) as temp:
env.write(temp)
temp_name = temp.name
env.write(temp, shell_file=temp_name)

# Evaluate env sourcing script and capture output of 'env'.
if os.name == 'nt':
Expand Down Expand Up @@ -164,15 +164,15 @@ def test_value_replacement(self):
self.env.set(self.var_not_set, '/foo/bar/baz')
self.env.add_replacement('FOOBAR', '/foo/bar')
buf = six.StringIO()
self.env.write(buf)
self.env.write(buf, shell_file='test.sh')
assert '/foo/bar' not in buf.getvalue()

def test_variable_replacement(self):
self.env.set('FOOBAR', '/foo/bar')
self.env.set(self.var_not_set, '/foo/bar/baz')
self.env.add_replacement('FOOBAR')
buf = six.StringIO()
self.env.write(buf)
self.env.write(buf, shell_file='test.sh')
print(buf.getvalue())
assert '/foo/bar/baz' not in buf.getvalue()

Expand Down
5 changes: 3 additions & 2 deletions pw_env_setup/py/pw_env_setup/env_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ def __init__(
)
self._cipd_cache_dir = cipd_cache_dir
self._shell_file = shell_file
self._env._shell_file = shell_file
self._is_windows = os.name == 'nt'
self._quiet = quiet
self._install_dir = install_dir
Expand Down Expand Up @@ -628,14 +629,14 @@ def setup(self):
return 0

with open(self._shell_file, 'w') as outs:
self._env.write(outs)
self._env.write(outs, shell_file=self._shell_file)

deactivate = os.path.join(
self._install_dir,
'deactivate{}'.format(os.path.splitext(self._shell_file)[1]),
)
with open(deactivate, 'w') as outs:
self._env.write_deactivate(outs)
self._env.write_deactivate(outs, shell_file=deactivate)

config = {
# Skipping sysname and nodename in os.uname(). nodename could change
Expand Down
21 changes: 16 additions & 5 deletions pw_env_setup/py/pw_env_setup/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ def __init__(self, *args, **kwargs):
self.replacements = []
self._join = Join(pathsep)
self._finalized = False
self._shell_file = ''

def add_replacement(self, variable, value=None):
self.replacements.append((variable, value))
Expand Down Expand Up @@ -440,7 +441,7 @@ def finalize(self):

if not self._windows:
buf = StringIO()
self.write_deactivate(buf)
self.write_deactivate(buf, shell_file=self._shell_file)
self._actions.append(Function('_pw_deactivate', buf.getvalue()))
self._blankline()

Expand All @@ -457,17 +458,27 @@ def gni(self, outs, project_root, gni_file):
def json(self, outs):
json_visitor.JSONVisitor().serialize(self, outs)

def write(self, outs):
def write(self, outs, shell_file):
if self._windows:
visitor = batch_visitor.BatchVisitor(pathsep=self._pathsep)
else:
visitor = shell_visitor.ShellVisitor(pathsep=self._pathsep)
if shell_file.endswith('.fish'):
visitor = shell_visitor.FishShellVisitor()
else:
visitor = shell_visitor.ShellVisitor(pathsep=self._pathsep)
visitor.serialize(self, outs)

def write_deactivate(self, outs):
def write_deactivate(self, outs, shell_file):
if self._windows:
return
visitor = shell_visitor.DeactivateShellVisitor(pathsep=self._pathsep)
if shell_file.endswith('.fish'):
visitor = shell_visitor.DeactivateFishShellVisitor(
pathsep=self._pathsep
)
else:
visitor = shell_visitor.DeactivateShellVisitor(
pathsep=self._pathsep
)
visitor.serialize(self, outs)

@contextlib.contextmanager
Expand Down
133 changes: 131 additions & 2 deletions pw_env_setup/py/pw_env_setup/shell_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def visit_hash(self, hash): # pylint: disable=redefined-builtin


class ShellVisitor(_BaseShellVisitor):
"""Serializes an Environment into a shell file."""
"""Serializes an Environment into a bash-like shell file."""

def __init__(self, *args, **kwargs):
super(ShellVisitor, self).__init__(*args, **kwargs)
Expand Down Expand Up @@ -174,7 +174,7 @@ def visit_function(self, function):


class DeactivateShellVisitor(_BaseShellVisitor):
"""Removes values from an Environment."""
"""Removes values from a bash-like shell environment."""

def __init__(self, *args, **kwargs):
pathsep = kwargs.pop('pathsep', ':')
Expand Down Expand Up @@ -227,3 +227,132 @@ def visit_blank_line(self, blank_line):

def visit_function(self, function):
pass # Not relevant.


class FishShellVisitor(ShellVisitor):
"""Serializes an Environment into a fish shell file."""

def __init__(self, *args, **kwargs):
super(FishShellVisitor, self).__init__(*args, **kwargs)
self._pathsep = ' '

def _remove_value_from_path(self, variable, value):
return 'set PATH (string match -v {value} ${variable})\n'.format(
variable=variable, value=value
)

def visit_set(self, set): # pylint: disable=redefined-builtin
value = self._apply_replacements(set)
self._outs.write(
'set -x {name} {value}\n'.format(name=set.name, value=value)
)

def visit_clear(self, clear):
self._outs.write('set -e {name}\n'.format(**vars(clear)))

def visit_remove(self, remove):
value = self._apply_replacements(remove)
self._remove_value_from_path(remove.name, value)

def visit_prepend(self, prepend):
value = self._apply_replacements(prepend)
self._outs.write(
'set -x --prepend {name} {value}\n'.format(
name=prepend.name, value=value
)
)

def visit_append(self, append):
value = self._apply_replacements(append)
self._outs.write(
'set -x --append {name} {value}\n'.format(
name=append.name, value=value
)
)

def visit_echo(self, echo):
self._outs.write('if not set -q PW_ENVSETUP_QUIET\n')
if echo.newline:
self._outs.write(' echo "{}"\n'.format(echo.value))
else:
self._outs.write(' echo -n "{}"\n'.format(echo.value))
self._outs.write('end\n')

def visit_hash(self, hash): # pylint: disable=redefined-builtin
del hash

def visit_function(self, function):
self._outs.write(
'function {name}\n{body}\nend\n'.format(
name=function.name, body=function.body
)
)

def visit_command(self, command):
self._outs.write('{}\n'.format(' '.join(command.command)))
if not command.exit_on_error:
return

# Assume failing command produced relevant output.
self._outs.write('if test $status -ne 0\n return 1\nend\n')

def visit_doctor(self, doctor):
self._outs.write('if not set -q PW_ACTIVATE_SKIP_CHECKS\n')
self.visit_command(doctor)
self._outs.write('else\n')
self._outs.write(
'echo Skipping environment check because '
'PW_ACTIVATE_SKIP_CHECKS is set\n'
)
self._outs.write('end\n')


class DeactivateFishShellVisitor(FishShellVisitor):
"""Removes values from a fish shell environment."""

def serialize(self, env, outs):
try:
self._outs = outs

env.accept(self)

finally:
self._outs = None

def visit_set(self, set): # pylint: disable=redefined-builtin
if set.deactivate:
self._outs.write('set -e {name}\n'.format(name=set.name))

def visit_clear(self, clear):
pass # Not relevant.

def visit_remove(self, remove):
pass # Not relevant.

def visit_prepend(self, prepend):
self._outs.write(
self._remove_value_from_path(prepend.name, prepend.value)
)

def visit_append(self, append):
self._outs.write(
self._remove_value_from_path(append.name, append.value)
)

def visit_echo(self, echo):
pass # Not relevant.

def visit_comment(self, comment):
pass # Not relevant.

def visit_command(self, command):
pass # Not relevant.

def visit_doctor(self, doctor):
pass # Not relevant.

def visit_blank_line(self, blank_line):
pass # Not relevant.

def visit_function(self, function):
pass # Not relevant.
Loading

0 comments on commit d988f70

Please sign in to comment.