Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build: build.jobs.post_build produces mysterious shell syntax errors #10103

Closed
rptb1 opened this issue Mar 4, 2023 · 14 comments · Fixed by #10119, #10133 or #10334
Closed

Build: build.jobs.post_build produces mysterious shell syntax errors #10103

rptb1 opened this issue Mar 4, 2023 · 14 comments · Fixed by #10119, #10133 or #10334
Assignees
Labels
Accepted Accepted issue on our roadmap Bug A bug

Comments

@rptb1
Copy link

rptb1 commented Mar 4, 2023

Details

Expected Result

A post_build script if test -n "A"; then echo "B"; fi should not produce a syntax error.
A post_build script with sh -c "foo" in it should run the shell with the argument.

Actual Result

/bin/sh: 1: Syntax error: "then" unexpected
sh: 0: -c requires an argument

I'm following the manual at "Extend the build process" and trying to insert a simple shell script at the post_build step.

A script that works locally (Ubuntu 22) is somehow mangled and causes syntax errors when inserted into the .readthedocs.yml at the post_build stage. I have tried many ways of writing the script and quoting it in YAML, to try to eliminate YAML as the culprit:

  1. Ravenbrook/mps@4146715
  2. Ravenbrook/mps@9fbc622
  3. Ravenbrook/mps@2a64259
  4. Ravenbrook/mps@7fdbbd3
  5. Ravenbrook/mps@82f0d18
  6. Ravenbrook/mps@044e790
  7. Ravenbrook/mps@4a07b84
  8. Ravenbrook/mps@1106c9c

The last one is especially mysterious.

Is there some sort of mangling of the script going on, or am I just going blind?

None of these scripts produce syntax errors when I feed them to /bin/sh on my local Ubuntu. I have written similar scripts for multiple CI systems without coming across this problem before.

Thanks.

@rptb1
Copy link
Author

rptb1 commented Mar 4, 2023

A literal copy of your own example produces a syntax error.

I coped the text of the script from https://docs.readthedocs.io/en/stable/build-customization.html#id3 exactly in Ravenbrook/mps@b08bcf3 and it causes the syntax error seen at https://readthedocs.org/projects/mmref/builds/19671564/

@humitos humitos changed the title post_build script produces mysterious shell syntax errors Build: build.jobs.post_build produces mysterious shell syntax errors Mar 6, 2023
@humitos
Copy link
Member

humitos commented Mar 6, 2023

Thanks for reporting this issue. We have been dealing with this already at pypi/warehouse#12953 and I wasn't able to find the cause of it at that time. We will need to dig deeper into the code to understand what's going on here.

@humitos humitos added Bug A bug Accepted Accepted issue on our roadmap labels Mar 6, 2023
@github-project-automation github-project-automation bot moved this to Planned in 📍Roadmap Mar 6, 2023
@rptb1
Copy link
Author

rptb1 commented Mar 6, 2023

We will need to dig deeper into the code to understand what's going on here.

This split looks highly suspicious.

environment.run(*command.split(), escape_command=False, cwd=cwd)

From a 10 minute scan of your code, it looks to me like there might be a confusion between executing a single command and executing a script.

On a single command, where you want to split argv and do something like run(['ls', '-l']), because you're basically doing something like a call to exec(3) and spawing a single process.

But a script is a single string that you want to pass complete to a shell, like run(['/bin/sh', '-c', SCRIPT]) (or similar). Splitting the script makes no sense at all.

>>> post_build = """
... if test -n "${MMREF}"; then \
...          mv "${READTHEDOCS_OUTPUT}/html" "${READTHEDOCS_OUTPUT}/mmref.in" && \
...          python manual/make-mmref.py "${READTHEDOCS_OUTPUT}/mmref.in" "${READTHEDOCS_OUTPUT}/html" \
...     fi
... """
>>> post_build.split()
['if', 'test', '-n', '"${MMREF}";', 'then', 'mv', '"${READTHEDOCS_OUTPUT}/html"', '"${READTHEDOCS_OUTPUT}/mmref.in"', '&&', 'python', 'manual/make-mmref.py', '"${READTHEDOCS_OUTPUT}/mmref.in"', '"${READTHEDOCS_OUTPUT}/html"', 'fi']

Again, I am in no way sure this is what's wrong, but your splits don't look right to me.

@ericholscher
Copy link
Member

ericholscher commented Mar 7, 2023

@rptb1 I'm curious if it works if you put the script in a file, and just run that from the post-build step? Not ideal, but I don't think our indention with this feature was having full-length scripts here, but having commands or scripts that could be run. We can definitely change that design, but curious if it works that way.

That said, if our docs examples aren't working, we should probably fix them :)

@rptb1
Copy link
Author

rptb1 commented Mar 7, 2023

That might be a workaround for now, thanks.

The intention is clear from the documentation: currently the documented examples don't work.

You'll need to carefully document the subset of shell language you do support. Why not just pass everything to the shell? Is there a reason not to?

humitos added a commit that referenced this issue Mar 7, 2023
I'm not sure about _why_ we split the commands before sending it out our
executor. The command is finally wrapper properly before being executed.

I did some locally QA after removing the split of the command and it seems these
scripts work fine. So, it seems safe to move forward with this, explanding the
flexibility of our config file.

Closes #10103
@humitos humitos self-assigned this Mar 7, 2023
@humitos humitos moved this from Planned to Needs review in 📍Roadmap Mar 7, 2023
@humitos
Copy link
Member

humitos commented Mar 7, 2023

I opened a PR that removes the .split() on those command and interprets the text as a whole. I did some small QA locally and it worked. I'd appreciate some review on that code since I'm not 100% sure that it's not gonna cause other issues.

humitos added a commit that referenced this issue Mar 7, 2023
…#10119)

* Build: pass shell commands directly (`build.jobs` / `build.commands)`

I'm not sure about _why_ we split the commands before sending it out our
executor. The command is finally wrapper properly before being executed.

I did some locally QA after removing the split of the command and it seems these
scripts work fine. So, it seems safe to move forward with this, explanding the
flexibility of our config file.

Closes #10103

* Test: match changes

It compares to a full command instead of being split.
@github-project-automation github-project-automation bot moved this from Needs review to Done in 📍Roadmap Mar 7, 2023
@humitos
Copy link
Member

humitos commented Mar 9, 2023

I'm re-opening this issue because the PR that was merged doesn't solve all the issues.
I'm still not sure what's the best way to solve this problem 🤷🏼 . This case, for example, is not solved:

Screenshot_2023-03-09_10-41-14

That exact same code works properly in my local terminal. Even wrapping it in /bin/sh -c '{cmd}' as we are doing in our code. It seems we are doing some manipulation of the command that breaks it, or, there is a problem when communicating with Docker.

This requires more researching and testing. Suggestions and ideas welcomed!

@humitos humitos reopened this Mar 9, 2023
@github-project-automation github-project-automation bot moved this from Done to In progress in 📍Roadmap Mar 9, 2023
@humitos
Copy link
Member

humitos commented Mar 9, 2023

I think the main problem here is the PATH= environment variable prefix we are adding:

It works without the prefix:

▶ sh -c 'if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- docs/ .readthedocs.yaml;
then
 exit 183;
fi'

It fails with the prefix

▶ sh -c 'PATH=/bin:$PATH if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- docs/ .readthedocs.yaml;
then
 exit 183;
fi'
sh: line 1: if: command not found
sh: -c: line 2: syntax error near unexpected token `then'
sh: -c: line 2: `then'

This code is at

def get_wrapped_command(self):
"""
Wrap command in a shell and optionally escape special bash characters.
In order to set the current working path inside a docker container, we
need to wrap the command in a shell call manually.
Some characters will be interpreted as shell characters without
escaping, such as: ``pip install requests<0.8``. When passing
``escape_command=True`` in the init method this escapes a good majority
of those characters.
"""
prefix = ''
if self.bin_path:
bin_path = self._escape_command(self.bin_path)
prefix += f'PATH={bin_path}:$PATH '
command = (
' '.join(
self._escape_command(part) if self.escape_command else part
for part in self.command
)
)
return (
"/bin/sh -c '{prefix}{cmd}'".format(
prefix=prefix,
cmd=command,
)
)

@humitos
Copy link
Member

humitos commented Mar 9, 2023

Another simplified example that breaks because of the environment variable prefix:

▶ sh -c 'if [ "1" ]; then echo 1; fi'
1

▶ sh -c 'FOO=bar if [ "1" ]; then echo 1; fi'
sh: -c: line 1: syntax error near unexpected token `then'
sh: -c: line 1: `FOO=bar if [ "1" ]; then echo 1; fi'

humitos added a commit that referenced this issue Mar 9, 2023
Instead of prepending all the commands with the `PATH=` variable, we pass the
environment variable directly to the Docker container.

This allow us to run multi-line shell script without failing with weird syntax
errors. Besides, the implementation is cleaner since all the environment
variables are passed to the commands in the same way.

I added some _default paths_ that I found by checking the local Docker
container. I'm also passing the users' path, depending if we are working locally
as root or in production.

This is not 100% complete and there may be some other issues that I'm not seeing
yet, but I think it's a first step to behave in a way our users are expecting.

Closes #10103
@humitos
Copy link
Member

humitos commented Mar 9, 2023

I opened 2 PRs to solve this issue:

With these two, I think we will be solving most of our users' issues avoiding unexpected behaviors.

@humitos humitos moved this from In progress to Needs review in 📍Roadmap Mar 9, 2023
@ericholscher
Copy link
Member

@humitos Great work tracking this down!

@github-project-automation github-project-automation bot moved this from Needs review to Done in 📍Roadmap Mar 16, 2023
humitos added a commit that referenced this issue Mar 16, 2023
* Build: pass `PATH` environment variable to Docker container

Instead of prepending all the commands with the `PATH=` variable, we pass the
environment variable directly to the Docker container.

This allow us to run multi-line shell script without failing with weird syntax
errors. Besides, the implementation is cleaner since all the environment
variables are passed to the commands in the same way.

I added some _default paths_ that I found by checking the local Docker
container. I'm also passing the users' path, depending if we are working locally
as root or in production.

This is not 100% complete and there may be some other issues that I'm not seeing
yet, but I think it's a first step to behave in a way our users are expecting.

Closes #10103

* Lint: for some reason fails at CircleCI otherwise

Locally it tries to reverted back 🤷

* Update readthedocs/doc_builder/environments.py

Co-authored-by: Santos Gallegos <[email protected]>

* Increase code's readability by applying suggested changes

---------

Co-authored-by: Santos Gallegos <[email protected]>
humitos added a commit that referenced this issue Mar 20, 2023
* Hosting: manual integrations via build contract

* Use a single script to load everything

* Include Read the Docs analytics to integrations

* Initial work for hosting features

* External version banner and doc-diff integration

* Old version warning

* Do not inject doc-diff on search page

* Inject old version warning only for non-external versions

* Comments!

* More comments

* Build: pass `PATH` environment variable to Docker container

Instead of prepending all the commands with the `PATH=` variable, we pass the
environment variable directly to the Docker container.

This allow us to run multi-line shell script without failing with weird syntax
errors. Besides, the implementation is cleaner since all the environment
variables are passed to the commands in the same way.

I added some _default paths_ that I found by checking the local Docker
container. I'm also passing the users' path, depending if we are working locally
as root or in production.

This is not 100% complete and there may be some other issues that I'm not seeing
yet, but I think it's a first step to behave in a way our users are expecting.

Closes #10103

* Lint: for some reason fails at CircleCI otherwise

Locally it tries to reverted back 🤷

* Feature flag for new hosting integrations

X-RTD-Hosting-Integration: true/false

This can be used from CloudFlare to decide whether or not inject a `<script>`
into the resulting HTML or not.

* Load `readthedocs-build.yaml` and generate `readthedocs-data.html`

* Load READTHEDOCS_DATA async

* Absolute proxied API path

* Remove duplicated code

* New approach using `readthedocs-client.js` and `/_/readthedocs-config.json`

See https://github.com/humitos/readthedocs-client

* Do not require `readthedocs-build.YAML` for now

* Expand the JSON response with more data

* Remove non-required files and rely on `readthedocs-client.js` only

* Improve helper text

* Builds: save `readthedocs-build.yaml` into database

I added a `Version.build_data` field that may be used from
`/_/readthedocs-config.json` to extend with data generated by the doctool at
build time if necessary.

* Use `Version.build_data` from the endpoint

* Flyout: return data required to generate flyout dynamically

* Updates to the API

* Minor updates

* Update the javascript client compiled version

* doc-diff object returned

* Build: check if the YAML file exists before trying to open it

* Proxito: don't inject the header if the feature is turned off

* Test: add hosting integrations tests

* Remove JS

This file will be deployed directly into S3.

* Load the javascript from a local server for now

* Update URL to remove .json from it

* Remove non-required f-string

* Allow saving `build_data` via API endpoint

* Lint

* Migrate nodejs installation to asdf

* Change port to match `npm run dev` from readthedocs-client
@humitos
Copy link
Member

humitos commented Mar 29, 2023

We deployed a fix twice for this but we had other issues, so we had to roll them back. I'm re-opening this issue because the problem is not fixed in production.

@humitos humitos reopened this Mar 29, 2023
@github-project-automation github-project-automation bot moved this from Done to In progress in 📍Roadmap Mar 29, 2023
humitos added a commit that referenced this issue Mar 29, 2023
We attemped to fix this issue multiple times and we found problems when
deploying those changes to production.

This not perfect, but simple solution, could help us to support multi-line
commands for now without manipulating "too much" the `PATH` variable,
that was the one causing us issues.

Closes #10103
@bensze01
Copy link

So, I've run into this issue as well (Mbed-TLS/mbedtls#7538) - The workaround I settled on is to insert a newline before the if statement.
However reading through @humitos's explanation, this probably breaks makes the PATH= statement ineffective.
I guess if passing this variable to docker had to be rolled back, you could make the commands look like this instead

export PATH=${foo}
${command_from_yaml}

@bensze01
Copy link

bensze01 commented May 11, 2023

Using the simplified example from @humitos:

$ sh -c 'FOO=bar if [ "1" ]; then echo 1; fi;'
sh: -c: line 1: syntax error near unexpected token 'then'
sh: -c: line 1: 'FOO=bar if [ "1" ]; then echo 1; fi;'

$ sh -c 'export FOO=bar
if [ "1" ]; then echo 1; fi;'
1

humitos added a commit that referenced this issue May 22, 2023
This is another attempt to fix this issue. It follows the suggestion posted by
@bensze01 in #10103. It
looks simple and I think it makes sense to give it a try since it doesn't seem
to be harmful.

I tested it locally with the branch
https://github.com/readthedocs/test-builds/tree/build-commands-multiline and it
worked fine. Note that without this PR the branch linked fails.

Related #10133
Related #10119
Related #10206

External PRs that should be solved by this one:
* pypi/warehouse#12953
* pypi/warehouse#12953

Closes #10103
Closes #10208
@github-project-automation github-project-automation bot moved this from In progress to Done in 📍Roadmap May 23, 2023
humitos added a commit that referenced this issue May 23, 2023
* Build: allow multi-line commands on `build.commands`

This is another attempt to fix this issue. It follows the suggestion posted by
@bensze01 in #10103. It
looks simple and I think it makes sense to give it a try since it doesn't seem
to be harmful.

I tested it locally with the branch
https://github.com/readthedocs/test-builds/tree/build-commands-multiline and it
worked fine. Note that without this PR the branch linked fails.

Related #10133
Related #10119
Related #10206

External PRs that should be solved by this one:
* pypi/warehouse#12953
* pypi/warehouse#12953

Closes #10103
Closes #10208

* Build: use `;` to separate PREFIX from COMMAND
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment