mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-28 14:10:37 +00:00
* fix(scripts): read the checkout .env as data in serve.sh
serve.sh sourced the root .env under set -a, so bash ran it as a script:
$$ became the shell's PID, backticks and $(...) ran commands, and an
unquoted value with a space ran its second word as a command (exit 127
under set -e, stopping make dev). The exports also beat the Gateway's
own load_dotenv(override=False), so e.g. a $$ in AUTH_JWT_SECRET changed
the signing secret on every launch.
Parse .env with a stdlib-only scripts/dotenv_exports.py on the host
Python and eval its shell-quoted export lines. It tokenizes like
python-dotenv, so values match what the Gateway reads, and keeps the
launcher's precedence over inherited variables and its $NAME /
${NAME:-x} / leading-~ expansion that support_bundle.py mirrors. $$ is
now one token in both. Unparseable lines and invalid names are skipped
with warnings that never echo values; a non-UTF-8 file stops the
launcher.
Tests run serve.sh's real .env block: no execution, parity with
python-dotenv, precedence, warnings, the non-UTF-8 stop and a stdlib-only
run; the support-bundle launcher cases now exercise the parser.
* docs(changelog): reference #7606 for serve.sh reading .env as data
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
174 lines
7.3 KiB
Python
174 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Print the checkout `.env` as shell `export` lines for `serve.sh`.
|
|
|
|
`serve.sh` used to `source` the file, so bash ran it as a script: `$$` became
|
|
the shell's PID, backticks and `$(...)` ran commands, and an unquoted value
|
|
with a space ran its second word as a command (exit 127 under `set -e`). The
|
|
Gateway reads the same file with python-dotenv but never overrides a variable
|
|
that is already exported, so the shell's reading won.
|
|
|
|
This reads the file as data instead and never executes any of it:
|
|
|
|
- Statements are tokenized like python-dotenv (comments, `export` prefixes,
|
|
single/double quotes and their escapes, multi-line quoted values, inline
|
|
comments after unquoted values), so a value means what the Gateway reads.
|
|
- The launcher's own semantics stay, as `support_bundle.py` mirrors them:
|
|
`.env` assignments override inherited variables, even with an empty value;
|
|
`$NAME`, `${NAME}` and `${NAME:-literal}` expand in unquoted and
|
|
double-quoted values from the environment, the checkout `PWD` and earlier
|
|
assignments; `\\$` is a literal dollar and `$$` stays as written; an
|
|
unquoted leading `~` expands; single-quoted values stay literal.
|
|
|
|
Each assignment is printed as `export NAME=<shell-quoted value>` for `eval`.
|
|
Statements that do not parse, and keys that are not shell variable names, are
|
|
skipped with a warning on stderr naming only the line or key, never a value.
|
|
An unreadable or non-UTF-8 file exits 1 before anything is printed.
|
|
|
|
Intentionally implemented with the standard library only: `serve.sh` runs this
|
|
with the host Python before `uv sync` has populated the venv.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import codecs
|
|
import os
|
|
import re
|
|
import shlex
|
|
import sys
|
|
from collections.abc import Mapping
|
|
from pathlib import Path
|
|
|
|
# Statement grammar of python-dotenv's parser (`dotenv/parser.py`).
|
|
_LEADING_SPACE = re.compile(r"\s*")
|
|
_EXPORT = re.compile(r"(?:export[^\S\r\n]+)?")
|
|
_SINGLE_QUOTED_KEY = re.compile(r"'([^']+)'")
|
|
_UNQUOTED_KEY = re.compile(r"([^=\#\s]+)")
|
|
_SPACE = re.compile(r"[^\S\r\n]*")
|
|
_EQUAL_SIGN = re.compile(r"=[^\S\r\n]*")
|
|
_SINGLE_QUOTED_VALUE = re.compile(r"'((?:\\'|[^'])*)'")
|
|
_DOUBLE_QUOTED_VALUE = re.compile(r'"((?:\\"|[^"])*)"')
|
|
_UNQUOTED_VALUE = re.compile(r"[^\r\n]*")
|
|
_COMMENT = re.compile(r"(?:[^\S\r\n]*#[^\r\n]*)?")
|
|
_END_OF_LINE = re.compile(r"[^\S\r\n]*(?:\r\n|\n|\r|$)")
|
|
_REST_OF_LINE = re.compile(r"[^\r\n]*(?:\r\n|\n|\r)?")
|
|
_SINGLE_QUOTE_ESCAPES = re.compile(r"\\[\\']")
|
|
_DOUBLE_QUOTE_ESCAPES = re.compile(r"\\[\\'\"abfnrtv]")
|
|
|
|
# The launcher's variable references, as support_bundle.py resolves them. `$$`
|
|
# is one token, as in the shell, so its second `$` never starts a reference.
|
|
_REFERENCE = re.compile(r"\\\$|\$\$|\$(?:\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}|([A-Za-z_][A-Za-z0-9_]*))")
|
|
_SHELL_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z")
|
|
|
|
|
|
class _Unparsed(Exception):
|
|
pass
|
|
|
|
|
|
def _match(pattern: re.Pattern[str], text: str, position: int) -> re.Match[str]:
|
|
match = pattern.match(text, position)
|
|
if match is None:
|
|
raise _Unparsed
|
|
return match
|
|
|
|
|
|
def _decode_escapes(pattern: re.Pattern[str], value: str) -> str:
|
|
return pattern.sub(lambda match: codecs.decode(match[0], "unicode-escape"), value)
|
|
|
|
|
|
def _expand_references(value: str, environment: Mapping[str, str]) -> str:
|
|
"""Expand simple shell references in one pass, without executing shell syntax."""
|
|
|
|
def replace(match: re.Match[str]) -> str:
|
|
if match[0] == "\\$":
|
|
return "$"
|
|
if match[0] == "$$":
|
|
return "$$"
|
|
resolved = environment.get(match[1] or match[3], "")
|
|
return match[2] if match[2] is not None and not resolved else resolved
|
|
|
|
return _REFERENCE.sub(replace, value)
|
|
|
|
|
|
def parse_dotenv(text: str, environment: Mapping[str, str]) -> list[tuple[str, str]]:
|
|
"""Return the `(name, value)` assignments in `text`, in file order."""
|
|
scope = dict(environment)
|
|
assignments: list[tuple[str, str]] = []
|
|
position = 0
|
|
while position < len(text):
|
|
position = _LEADING_SPACE.match(text, position).end()
|
|
if position >= len(text):
|
|
break
|
|
line = text.count("\n", 0, position) + 1
|
|
try:
|
|
position = _EXPORT.match(text, position).end()
|
|
key = None
|
|
if text.startswith("'", position):
|
|
match = _match(_SINGLE_QUOTED_KEY, text, position)
|
|
key, position = match[1], match.end()
|
|
elif not text.startswith("#", position):
|
|
match = _match(_UNQUOTED_KEY, text, position)
|
|
key, position = match[1], match.end()
|
|
position = _SPACE.match(text, position).end()
|
|
quote = ""
|
|
value = None
|
|
if text.startswith("=", position):
|
|
position = _EQUAL_SIGN.match(text, position).end()
|
|
quote = text[position : position + 1]
|
|
if quote == "'":
|
|
match = _match(_SINGLE_QUOTED_VALUE, text, position)
|
|
value = _decode_escapes(_SINGLE_QUOTE_ESCAPES, match[1])
|
|
elif quote == '"':
|
|
match = _match(_DOUBLE_QUOTED_VALUE, text, position)
|
|
value = _decode_escapes(_DOUBLE_QUOTE_ESCAPES, match[1])
|
|
else:
|
|
match = _UNQUOTED_VALUE.match(text, position)
|
|
value = re.sub(r"\s+#.*", "", match[0]).rstrip()
|
|
position = match.end()
|
|
position = _COMMENT.match(text, position).end()
|
|
position = _match(_END_OF_LINE, text, position).end()
|
|
except _Unparsed:
|
|
print(f"dotenv_exports: skipping a statement that does not parse at line {line}", file=sys.stderr)
|
|
position = _REST_OF_LINE.match(text, position).end()
|
|
continue
|
|
# A comment, or a bare name without `=`, assigns nothing.
|
|
if key is None or value is None:
|
|
continue
|
|
if not _SHELL_NAME.match(key):
|
|
print(f"dotenv_exports: skipping {key!r} at line {line}: not a shell variable name", file=sys.stderr)
|
|
continue
|
|
if quote != "'":
|
|
if quote != '"':
|
|
value = os.path.expanduser(value)
|
|
value = _expand_references(value, scope)
|
|
scope[key] = value
|
|
assignments.append((key, value))
|
|
return assignments
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = sys.argv[1:] if argv is None else argv
|
|
if len(args) != 1:
|
|
print("usage: dotenv_exports.py <path-to-.env>", file=sys.stderr)
|
|
return 2
|
|
path = Path(args[0])
|
|
try:
|
|
# utf-8-sig drops a leading BOM, which would otherwise join the first key.
|
|
text = path.read_text(encoding="utf-8-sig")
|
|
except UnicodeDecodeError:
|
|
print(f"dotenv_exports: {path} is not valid UTF-8", file=sys.stderr)
|
|
return 1
|
|
except OSError as exc:
|
|
print(f"dotenv_exports: cannot read {path}: {exc.strerror or exc}", file=sys.stderr)
|
|
return 1
|
|
environment = dict(os.environ)
|
|
# serve.sh runs this from the checkout, so `$PWD` resolves against it.
|
|
environment.setdefault("PWD", os.getcwd())
|
|
exports = "".join(f"export {name}={shlex.quote(value)}\n" for name, value in parse_dotenv(text, environment))
|
|
# Bytes with "\n" endings: Windows text mode would add a "\r" to every value.
|
|
sys.stdout.buffer.write(exports.encode("utf-8"))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|