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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-12464-987715e4a92ae46f391cb6a291c0beef0625fff3
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit a140a0015baf0d0e78b23d12b53e7f9e0d701f91
Author: hutiefang76 <[email protected]>
AuthorDate: Fri Oct 9 09:25:59 2026 +0000

    [Improve][CLI] Install source setup in a project virtual environment 
(#12464)
---
 .gitignore                               |   3 +
 docs/en/ai-cli/quickstart.md             |   1 +
 docs/zh/ai-cli/quickstart.md             |   1 +
 seatunnel-cli/README.md                  |   3 +-
 seatunnel-cli/README.zh-CN.md            |   3 +-
 seatunnel-cli/setup.sh                   |  52 +++++++++++--
 seatunnel-cli/tests/test_setup_script.py | 129 +++++++++++++++++++++++++++++++
 7 files changed, 184 insertions(+), 8 deletions(-)

diff --git a/.gitignore b/.gitignore
index b1eba9b18d..6e54678864 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,6 +51,9 @@ apidoc
 
 # Python
 *.py[cod]
+seatunnel-cli/.venv
+seatunnel-cli/.venv.invalid.*
+seatunnel-cli/*.egg-info/
 
 Test.java
 Test.scala
diff --git a/docs/en/ai-cli/quickstart.md b/docs/en/ai-cli/quickstart.md
index a513ec07b9..d6f0bab50d 100644
--- a/docs/en/ai-cli/quickstart.md
+++ b/docs/en/ai-cli/quickstart.md
@@ -36,6 +36,7 @@ bin/seatunnel-ai.sh
 ```bash
 cd seatunnel-cli
 bash setup.sh          # installs all providers + dev tools
+source .venv/bin/activate
 seatunnel --init       # interactive provider setup
 ```
 
diff --git a/docs/zh/ai-cli/quickstart.md b/docs/zh/ai-cli/quickstart.md
index 02806de4bc..e1ce2dccc3 100644
--- a/docs/zh/ai-cli/quickstart.md
+++ b/docs/zh/ai-cli/quickstart.md
@@ -36,6 +36,7 @@ bin/seatunnel-ai.sh
 ```bash
 cd seatunnel-cli
 bash setup.sh          # 安装全部提供商依赖 + 开发工具
+source .venv/bin/activate
 seatunnel --init       # 交互式配置提供商
 ```
 
diff --git a/seatunnel-cli/README.md b/seatunnel-cli/README.md
index ccb6cf87c8..9db8ce6314 100644
--- a/seatunnel-cli/README.md
+++ b/seatunnel-cli/README.md
@@ -55,9 +55,10 @@ cd seatunnel-cli
 
 # Quick setup (installs all providers + dev tools)
 bash setup.sh
+source .venv/bin/activate
 ```
 
-Then configure your LLM provider:
+The setup script installs into `seatunnel-cli/.venv` and reuses that 
environment on later runs. Activate it before using `seatunnel`, or call 
`.venv/bin/seatunnel` directly. It does not install packages into the system 
Python. Then configure your LLM provider:
 
 ```bash
 seatunnel --init
diff --git a/seatunnel-cli/README.zh-CN.md b/seatunnel-cli/README.zh-CN.md
index 7745f891a2..585d591b10 100644
--- a/seatunnel-cli/README.zh-CN.md
+++ b/seatunnel-cli/README.zh-CN.md
@@ -54,9 +54,10 @@ cd seatunnel-cli
 
 # 快速安装(安装所有提供商 + 开发工具)
 bash setup.sh
+source .venv/bin/activate
 ```
 
-然后配置 LLM 提供商:
+脚本会将依赖安装到 `seatunnel-cli/.venv`,再次运行时复用该环境。使用 `seatunnel` 前请先激活环境,也可直接运行 
`.venv/bin/seatunnel`;脚本不会把依赖安装到系统 Python。然后配置 LLM 提供商:
 
 ```bash
 seatunnel --init
diff --git a/seatunnel-cli/setup.sh b/seatunnel-cli/setup.sh
index 07703bdde6..e01dea5dc1 100644
--- a/seatunnel-cli/setup.sh
+++ b/seatunnel-cli/setup.sh
@@ -18,7 +18,9 @@
 
 # SeaTunnel CLI - Quick Setup Script
 
-set -e
+set -euo pipefail
+
+cd "$(dirname "${BASH_SOURCE[0]}")"
 
 echo "=== SeaTunnel CLI Setup ==="
 echo ""
@@ -27,22 +29,60 @@ echo ""
 PYTHON_VERSION=$(python3 -c 'import sys; 
print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null) || {
     echo "Error: Python 3.10+ required but python3 not found"; exit 1;
 }
-PYTHON_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2)
-if [ "$PYTHON_MINOR" -lt 10 ]; then
+PYTHON_MAJOR=${PYTHON_VERSION%%.*}
+PYTHON_MINOR=${PYTHON_VERSION#*.}
+if [ "$PYTHON_MAJOR" -lt 3 ] || { [ "$PYTHON_MAJOR" -eq 3 ] && [ 
"$PYTHON_MINOR" -lt 10 ]; }; then
     echo "Error: Python >= 3.10 required, but found Python $PYTHON_VERSION"
     exit 1
 fi
 echo "Python $PYTHON_VERSION"
 
-# Install in development mode
-echo "Installing seatunnel CLI..."
-python3 -m pip install -e ".[dev]"
+VENV_DIR="$PWD/.venv"
+venv_usable() {
+    [ -x "$VENV_DIR/bin/python" ] &&
+        "$VENV_DIR/bin/python" -c 'import sys; assert sys.prefix != 
sys.base_prefix and sys.version_info >= (3, 10)' >/dev/null 2>&1 &&
+        "$VENV_DIR/bin/python" -m pip --version >/dev/null 2>&1
+}
+
+if venv_usable; then
+    echo "Reusing $VENV_DIR"
+else
+    # Preserve a pre-existing but unusable environment for inspection.
+    VENV_BACKUP=""
+    if [ -e "$VENV_DIR" ] || [ -L "$VENV_DIR" ]; then
+        VENV_BACKUP="${VENV_DIR}.invalid.$(date +%s).$$"
+        mv "$VENV_DIR" "$VENV_BACKUP"
+        echo "Preserved unusable environment at $VENV_BACKUP"
+    fi
+
+    if ! python3 -m venv "$VENV_DIR" || ! venv_usable; then
+        # A failed venv attempt can leave files that virtualenv will not 
overwrite.
+        rm -rf -- "$VENV_DIR"
+        if ! command -v virtualenv >/dev/null 2>&1 ||
+            ! virtualenv --python=python3 "$VENV_DIR" || ! venv_usable; then
+            rm -rf -- "$VENV_DIR"
+            if [ -n "$VENV_BACKUP" ]; then
+                mv "$VENV_BACKUP" "$VENV_DIR"
+            fi
+            echo "Error: Python virtual environments require python3-venv 
(ensurepip) or virtualenv." >&2
+            exit 1
+        fi
+    fi
+
+fi
+
+echo "Installing seatunnel CLI in $VENV_DIR..."
+"$VENV_DIR/bin/python" -m pip install --upgrade pip setuptools wheel
+"$VENV_DIR/bin/python" -m pip install -e ".[dev]"
 
 echo ""
 echo "=== Setup Complete ==="
 echo ""
 echo "Next steps:"
 echo ""
+echo "  Activate this checkout's environment, or call .venv/bin/seatunnel 
directly:"
+echo "     source .venv/bin/activate"
+echo ""
 echo "  1. Set SeaTunnel engine path (for /check, /run, connector metadata):"
 echo ""
 echo "     export SEATUNNEL_HOME=/path/to/apache-seatunnel"
diff --git a/seatunnel-cli/tests/test_setup_script.py 
b/seatunnel-cli/tests/test_setup_script.py
new file mode 100644
index 0000000000..8bc1daeeea
--- /dev/null
+++ b/seatunnel-cli/tests/test_setup_script.py
@@ -0,0 +1,129 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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
+#
+#    http://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.
+
+"""Behavioral tests for the source-install setup script without downloading 
packages."""
+
+import os
+import shutil
+import subprocess
+import tempfile
+import unittest
+from pathlib import Path
+
+
+SETUP_SCRIPT = Path(__file__).resolve().parents[1] / "setup.sh"
+
+
+class SetupScriptTest(unittest.TestCase):
+    def setUp(self):
+        self.temp_dir = tempfile.TemporaryDirectory()
+        self.addCleanup(self.temp_dir.cleanup)
+        self.root = Path(self.temp_dir.name)
+        self.cli_dir = self.root / "seatunnel-cli"
+        self.cli_dir.mkdir()
+        shutil.copy2(SETUP_SCRIPT, self.cli_dir / "setup.sh")
+        self.bin_dir = self.root / "bin"
+        self.bin_dir.mkdir()
+        self.log = self.root / "calls.log"
+        self._write_executable(
+            "venv-python",
+            """#!/usr/bin/env bash
+echo "venv-python $*" >> "$SETUP_TEST_LOG"
+exit 0
+""",
+        )
+        self._write_executable(
+            "python3",
+            """#!/usr/bin/env bash
+if [ "$1" = "-c" ]; then echo 3.11; exit 0; fi
+echo "python3 $*" >> "$SETUP_TEST_LOG"
+if [ "$1" = "-m" ] && [ "$2" = "venv" ]; then
+    mkdir -p "$3/bin"
+    if [ "$SETUP_TEST_MODE" = "fallback" ] || [ "$SETUP_TEST_MODE" = "fail" ]; 
then
+        touch "$3/partial"
+        exit 1
+    fi
+    cp "$SETUP_TEST_BIN/venv-python" "$3/bin/python"
+    exit 0
+fi
+exit 1
+""",
+        )
+        self._write_executable(
+            "virtualenv",
+            """#!/usr/bin/env bash
+echo "virtualenv $*" >> "$SETUP_TEST_LOG"
+dest="${@: -1}"
+if [ -e "$dest/partial" ] || [ "$SETUP_TEST_MODE" = "fail" ]; then exit 1; fi
+mkdir -p "$dest/bin"
+cp "$SETUP_TEST_BIN/venv-python" "$dest/bin/python"
+""",
+        )
+
+    def _write_executable(self, name, content):
+        path = self.bin_dir / name
+        path.write_text(content, encoding="utf-8")
+        path.chmod(0o755)
+
+    def _run_setup(self, mode="normal"):
+        env = os.environ.copy()
+        env.update({
+            "PATH": str(self.bin_dir) + os.pathsep + env["PATH"],
+            "SETUP_TEST_LOG": str(self.log),
+            "SETUP_TEST_BIN": str(self.bin_dir),
+            "SETUP_TEST_MODE": mode,
+        })
+        return subprocess.run(
+            ["bash", str(self.cli_dir / "setup.sh")],
+            cwd=self.root,
+            env=env,
+            text=True,
+            capture_output=True,
+            check=False,
+        )
+
+    def test_creates_isolated_environment_and_reuses_it(self):
+        first = self._run_setup()
+        self.assertEqual(first.returncode, 0, first.stderr)
+        self.assertTrue((self.cli_dir / ".venv/bin/python").exists())
+        self.assertIn("source .venv/bin/activate", first.stdout)
+        second = self._run_setup()
+        self.assertEqual(second.returncode, 0, second.stderr)
+        calls = self.log.read_text(encoding="utf-8")
+        self.assertEqual(calls.count("python3 -m venv"), 1)
+        self.assertEqual(calls.count("venv-python -m pip install --upgrade"), 
2)
+        self.assertEqual(calls.count("venv-python -m pip install -e .[dev]"), 
2)
+        self.assertNotIn("python3 -m pip", calls)
+
+    def test_removes_partial_venv_before_virtualenv_fallback(self):
+        result = self._run_setup("fallback")
+        self.assertEqual(result.returncode, 0, result.stderr)
+        self.assertTrue((self.cli_dir / ".venv/bin/python").exists())
+        self.assertFalse((self.cli_dir / ".venv/partial").exists())
+        self.assertIn("virtualenv --python=python3", 
self.log.read_text(encoding="utf-8"))
+
+    def test_reports_missing_venv_support_without_global_install(self):
+        old = self.cli_dir / ".venv"
+        old.mkdir()
+        (old / "important-marker").write_text("keep", encoding="utf-8")
+        result = self._run_setup("fail")
+        self.assertNotEqual(result.returncode, 0)
+        self.assertIn("python3-venv (ensurepip) or virtualenv", result.stderr)
+        self.assertEqual((old / 
"important-marker").read_text(encoding="utf-8"), "keep")
+        self.assertNotIn("python3 -m pip", 
self.log.read_text(encoding="utf-8"))
+
+
+if __name__ == "__main__":
+    unittest.main()

Reply via email to