Compare commits

..

4 Commits

Author SHA1 Message Date
Xuwznln
81e9068597 support notebook id 2026-05-20 18:14:13 +08:00
Xuwznln
be5ff9bc5c new build fix 2026-05-14 19:28:05 +08:00
Xuwznln
498bcd84f8 v0.11.2
(cherry picked from commit bcb1790897)
2026-05-14 18:22:09 +08:00
Xuwznln
35199eb863 env installation fix 2026-05-14 18:18:53 +08:00
18 changed files with 249 additions and 47 deletions

View File

@@ -3,7 +3,7 @@
package:
name: unilabos
version: 0.11.1
version: 0.11.2
source:
path: ../../unilabos
@@ -54,7 +54,7 @@ requirements:
- pymodbus
- matplotlib
- pylibftdi
- uni-lab::unilabos-env ==0.11.1
- uni-lab::unilabos-env ==0.11.2
about:
repository: https://github.com/deepmodeling/Uni-Lab-OS

View File

@@ -2,7 +2,7 @@
package:
name: unilabos-env
version: 0.11.1
version: 0.11.2
build:
noarch: generic

View File

@@ -3,7 +3,7 @@
package:
name: unilabos-full
version: 0.11.1
version: 0.11.2
build:
noarch: generic
@@ -11,7 +11,7 @@ build:
requirements:
run:
# Base unilabos package (includes unilabos-env)
- uni-lab::unilabos ==0.11.1
- uni-lab::unilabos ==0.11.2
# Documentation tools
- sphinx
- sphinx_rtd_theme

View File

@@ -25,7 +25,7 @@ jobs:
fetch-depth: 0
- name: Setup Miniforge
uses: conda-incubator/setup-miniconda@v4
uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
use-mamba: true

View File

@@ -86,7 +86,7 @@ jobs:
- name: Setup Miniforge (with mamba)
if: steps.should_build.outputs.should_build == 'true'
uses: conda-incubator/setup-miniconda@v4
uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
use-mamba: true

View File

@@ -51,7 +51,7 @@ jobs:
fetch-depth: 0
- name: Setup Miniforge (with mamba)
uses: conda-incubator/setup-miniconda@v4
uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
use-mamba: true

View File

@@ -101,10 +101,11 @@ jobs:
- name: Setup Miniforge
if: steps.should_build.outputs.should_build == 'true'
uses: conda-incubator/setup-miniconda@v4
uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
use-mamba: true
python-version: '3.11.14'
channels: conda-forge,robostack-staging
channel-priority: strict
activate-environment: build-env
@@ -114,13 +115,15 @@ jobs:
- name: Install rattler-build and anaconda-client
if: steps.should_build.outputs.should_build == 'true'
run: |
mamba install --override-channels -c conda-forge rattler-build anaconda-client -y
mamba install -n build-env --override-channels -c conda-forge rattler-build anaconda-client -y
- name: Show environment info
if: steps.should_build.outputs.should_build == 'true'
run: |
conda info
conda list | grep -E "(rattler-build|anaconda-client)"
conda list -n build-env | grep -E "(rattler-build|anaconda-client)"
conda run -n build-env rattler-build --version
conda run -n build-env anaconda --version
echo "Platform: ${{ matrix.platform }}"
echo "OS: ${{ matrix.os }}"
@@ -128,9 +131,9 @@ jobs:
if: steps.should_build.outputs.should_build == 'true'
run: |
if [[ "${{ matrix.platform }}" == "osx-arm64" ]]; then
rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
conda run -n build-env rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
else
rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
conda run -n build-env rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
fi
- name: List built packages
@@ -171,5 +174,5 @@ jobs:
run: |
for package in $(find ./output -name "*.conda"); do
echo "Uploading $package to unilab organization..."
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
done

View File

@@ -94,10 +94,11 @@ jobs:
- name: Setup Miniforge
if: steps.should_build.outputs.should_build == 'true'
uses: conda-incubator/setup-miniconda@v4
uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
use-mamba: true
python-version: '3.11.14'
channels: conda-forge,robostack-staging,uni-lab
channel-priority: strict
activate-environment: build-env
@@ -107,13 +108,15 @@ jobs:
- name: Install rattler-build and anaconda-client
if: steps.should_build.outputs.should_build == 'true'
run: |
mamba install --override-channels -c conda-forge rattler-build anaconda-client -y
mamba install -n build-env --override-channels -c conda-forge rattler-build anaconda-client -y
- name: Show environment info
if: steps.should_build.outputs.should_build == 'true'
run: |
conda info
conda list | grep -E "(rattler-build|anaconda-client)"
conda list -n build-env | grep -E "(rattler-build|anaconda-client)"
conda run -n build-env rattler-build --version
conda run -n build-env anaconda --version
echo "Platform: ${{ matrix.platform }}"
echo "OS: ${{ matrix.os }}"
echo "Build full package: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' }}"
@@ -128,7 +131,7 @@ jobs:
if: steps.should_build.outputs.should_build == 'true'
run: |
echo "Building unilabos-env (conda environment dependencies)..."
rattler-build build -r .conda/environment/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge
conda run -n build-env rattler-build build -r .conda/environment/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge
- name: Upload unilabos-env to Anaconda.org (if enabled)
if: |
@@ -140,7 +143,7 @@ jobs:
run: |
echo "Uploading unilabos-env to uni-lab organization..."
for package in $(find ./output -name "unilabos-env*.conda"); do
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
done
- name: Build unilabos (with pip package)
@@ -148,7 +151,7 @@ jobs:
run: |
echo "Building unilabos package..."
# 如果已上传到 Anaconda从 uni-lab channel 获取 unilabos-env否则从本地 output 获取
rattler-build build -r .conda/base/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
conda run -n build-env rattler-build build -r .conda/base/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
- name: Upload unilabos to Anaconda.org (if enabled)
if: |
@@ -160,7 +163,7 @@ jobs:
run: |
echo "Uploading unilabos to uni-lab organization..."
for package in $(find ./output -name "unilabos-0*.conda" -o -name "unilabos-[0-9]*.conda"); do
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
done
- name: Build unilabos-full - Only when explicitly requested
@@ -170,7 +173,7 @@ jobs:
github.event.inputs.build_full == 'true'
run: |
echo "Building unilabos-full package on ${{ matrix.platform }}..."
rattler-build build -r .conda/full/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
conda run -n build-env rattler-build build -r .conda/full/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
- name: Upload unilabos-full to Anaconda.org (if enabled)
if: |
@@ -181,7 +184,7 @@ jobs:
run: |
echo "Uploading unilabos-full to uni-lab organization..."
for package in $(find ./output -name "unilabos-full*.conda"); do
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
done
- name: List built packages

View File

@@ -1,6 +1,6 @@
package:
name: ros-humble-unilabos-msgs
version: 0.11.1
version: 0.11.2
source:
path: ../../unilabos_msgs
target_directory: src

View File

@@ -1,6 +1,6 @@
package:
name: unilabos
version: "0.11.1"
version: "0.11.2"
source:
path: ../..

View File

@@ -4,7 +4,7 @@ package_name = 'unilabos'
setup(
name=package_name,
version='0.11.1',
version='0.11.2',
packages=find_packages(),
include_package_data=True,
install_requires=['setuptools'],

View File

@@ -1 +1 @@
__version__ = "0.11.1"
__version__ = "0.11.2"

View File

@@ -59,6 +59,7 @@ class JobAddReq(BaseModel):
task_id: str = Field(examples=["task_id"], description="task uuid (auto-generated if empty)", default="")
job_id: str = Field(examples=["job_id"], description="goal uuid (auto-generated if empty)", default="")
node_id: str = Field(examples=["node_id"], description="node uuid", default="")
notebook_id: str = Field(examples=["notebook_id"], description="notebook uuid", default="")
server_info: dict = Field(
examples=[{"send_timestamp": 1717000000.0}],
description="server info (auto-generated if empty)",

View File

@@ -10,29 +10,170 @@ import shutil
import sys
_PATCH_MARKER = "# UniLabOS DLL Patch"
_PATCH_END_MARKER = "# End UniLabOS DLL Patch"
# 75 = EX_TEMPFAIL: 临时失败、重试即可,避免与业务退出码冲突
_RESTART_EXIT_CODE = 75
def _build_dll_patch(lib_bin: str, preload_pyd: str = "") -> str:
"""生成一段加在目标文件顶部的 DLL 加载补丁源码。
- 始终把 ``lib_bin`` 加入 DLL 搜索路径,并把 handle 挂在模块属性上,
防止 GC 清掉搜索路径(``os.add_dll_directory`` 的句柄被回收时
目录会被移除)。
- 可选地用 ``ctypes.CDLL`` 预加载一个 .pyd把它的依赖 DLL 提前装入
进程内存,作为 ``rclpy._rclpy_pybind11`` 这类首次加载点的兜底。
"""
# 用 repr() 序列化路径Python 解析 repr 的结果会还原成原始字符串,
# 不需要也不能再叠加 raw-string 前缀(叠了反而会让 \\ 变成两个反斜杠)。
lines = [
_PATCH_MARKER,
"import os as _ulab_os",
f"_ulab_p = {lib_bin!r}",
'if hasattr(_ulab_os, "add_dll_directory") and _ulab_os.path.isdir(_ulab_p):',
" try: _UNILAB_DLL_HANDLE = _ulab_os.add_dll_directory(_ulab_p)",
" except Exception: _UNILAB_DLL_HANDLE = None",
]
if preload_pyd:
lines.extend(
[
"import ctypes as _ulab_ctypes",
f"try: _ulab_ctypes.CDLL({preload_pyd!r})",
"except Exception: pass",
]
)
lines.append(_PATCH_END_MARKER)
return "\n".join(lines) + "\n"
def _apply_dll_patch(file_path: str, lib_bin: str, preload_pyd: str = "") -> bool:
"""把 DLL 补丁前置到 ``file_path``。文件不存在或已打过补丁则返回 False。"""
if not os.path.isfile(file_path):
return False
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
if _PATCH_MARKER in content:
return False
shutil.copy2(file_path, file_path + ".bak")
with open(file_path, "w", encoding="utf-8") as f:
f.write(_build_dll_patch(lib_bin, preload_pyd) + content)
return True
def _print_restart_banner(patched_files):
"""打印重启提示并以 EX_TEMPFAIL 退出。
- 不使用 ANSI 颜色码Windows 旧版 cmd / PowerShell 5 默认不开 VT 处理,
会把 ``\\033[1;33m`` 当做字面字符显示,反而让用户看不到正文。
- 同时写入 stderr 与 stdout某些上层 launcher / supervisor 只重定向
其中一路,写两遍能保证用户至少看到一份。
- 写入前防御性把流切到 UTF-8 with replace``main.py`` 里已经做过一次,
但本模块也可能被绕过 ``main.py`` 的代码路径直接 importreconfigure
失败也只是退回 errors=replace不影响整体流程。
"""
if sys.platform == "win32":
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
except (AttributeError, OSError):
pass
bar = "#" * 78
files_lines = [f"[UniLabOS] - {p}" for p in patched_files]
body = "\n".join(
[
"",
bar,
bar,
"##",
"## [UniLabOS] Windows + conda 下检测到 DLL 加载失败,已自动打补丁。",
"## [UniLabOS] DLL load failure detected on Windows + conda;",
"## [UniLabOS] the following files have been auto-patched:",
"##",
*[f"## {line}" for line in files_lines],
"##",
"## [UniLabOS] 当前进程的 rclpy 状态已损坏,补丁需要在新进程才生效。",
"## [UniLabOS] The current process is unusable; the patch only takes",
"## [UniLabOS] effect on a fresh process.",
"##",
"## >>> 请重新运行刚才的命令 / Please re-run the same command. <<<",
"##",
bar,
bar,
"",
]
)
for stream in (sys.stderr, sys.stdout):
try:
stream.write(body)
stream.flush()
except Exception:
try:
print(body, file=stream)
except Exception:
pass
sys.exit(_RESTART_EXIT_CODE)
def patch_rclpy_dll_windows():
"""在 Windows + conda 环境下 rclpy 打 DLL 加载补丁"""
"""在 Windows + conda 环境下修复 rclpy / rosidl typesupport 的 DLL 加载。
背景conda 安装的 ros 系列包,其原生扩展依赖 ``$CONDA_PREFIX/Library/bin``
下的 DLL只有 conda 环境被正确激活、且 PATH 中含 ``Library/bin`` 时,
``os.add_dll_directory`` 才能找到它们。当从快捷方式 / IDE / 子进程 /
没激活的 shell 启动 ``unilab`` 时,会出现 ``DLL load failed``。
本函数会:
1) 修补 ``rclpy/impl/implementation_singleton.py`` —— rclpy 自身的 C 扩展入口;
2) 修补 ``rpyutils/add_dll_directories.py`` —— 所有 ``*_s__rosidl_typesupport_c.pyd``
``geometry_msgs`` / ``std_msgs`` / ``sensor_msgs`` 等)的统一加载入口。
打完补丁后**必须重启进程**才能生效(当前进程的 rclpy 已经发生过
``ImportError``,子模块仍处于损坏状态)。因此函数会主动退出,并在
stdout/stderr 同时打印明显的重启提示,避免用户被后续报错淹没。
"""
if sys.platform != "win32" or not os.environ.get("CONDA_PREFIX"):
return
try:
import rclpy
import rclpy # noqa: F401
return
except ImportError as e:
if not str(e).startswith("DLL load failed"):
return
cp = os.environ["CONDA_PREFIX"]
impl = os.path.join(cp, "Lib", "site-packages", "rclpy", "impl", "implementation_singleton.py")
pyd = glob.glob(os.path.join(cp, "Lib", "site-packages", "rclpy", "_rclpy_pybind11*.pyd"))
if not os.path.exists(impl) or not pyd:
lib_bin = os.path.join(cp, "Library", "bin")
site_packages = os.path.join(cp, "Lib", "site-packages")
if not os.path.isdir(lib_bin):
return
with open(impl, "r", encoding="utf-8") as f:
content = f.read()
lib_bin = os.path.join(cp, "Library", "bin").replace("\\", "/")
patch = f'# UniLabOS DLL Patch\nimport os,ctypes\nos.add_dll_directory("{lib_bin}") if hasattr(os,"add_dll_directory") else None\ntry: ctypes.CDLL("{pyd[0].replace(chr(92),"/")}")\nexcept: pass\n# End Patch\n'
shutil.copy2(impl, impl + ".bak")
with open(impl, "w", encoding="utf-8") as f:
f.write(patch + content)
patched = []
# 1) rclpy 自身的入口
rclpy_impl = os.path.join(site_packages, "rclpy", "impl", "implementation_singleton.py")
rclpy_pyd_matches = glob.glob(os.path.join(site_packages, "rclpy", "_rclpy_pybind11*.pyd"))
rclpy_pyd = rclpy_pyd_matches[0] if rclpy_pyd_matches else ""
if rclpy_pyd and _apply_dll_patch(rclpy_impl, lib_bin, preload_pyd=rclpy_pyd):
patched.append(rclpy_impl)
# 2) rpyutils —— 所有 rosidl typesupport pyd 的加载点;放在 rclpy 之后
# 例geometry_msgs/geometry_msgs_s__rosidl_typesupport_c.pyd
rpyutils_dll = os.path.join(site_packages, "rpyutils", "add_dll_directories.py")
if _apply_dll_patch(rpyutils_dll, lib_bin):
patched.append(rpyutils_dll)
if not patched:
# 已经打过补丁但 rclpy 仍然加载失败:原因不是缺 DLL 搜索路径,
# 不要再次打补丁污染文件,让上层看到真实的 ImportError。
return
_print_restart_banner(patched)
patch_rclpy_dll_windows()

View File

@@ -320,6 +320,7 @@ def job_add(req: JobAddReq) -> JobData:
action_name=action_name,
task_id=task_id,
job_id=job_id,
notebook_id=req.notebook_id,
device_action_key=device_action_key,
)

View File

@@ -59,6 +59,7 @@ class QueueItem:
action_name: str
task_id: str
job_id: str
notebook_id: str
device_action_key: str
next_run_time: float = 0 # 下次执行时间戳
retry_count: int = 0 # 重试次数
@@ -71,6 +72,7 @@ class JobInfo:
job_id: str
task_id: str
device_id: str
notebook_id: str
action_name: str
device_action_key: str
status: JobStatus
@@ -539,7 +541,10 @@ class MessageProcessor:
self.reconnect_count += 1
backoff = WSConfig.reconnect_interval
logger.info(
f"[MessageProcessor] 即将在 {backoff} 秒后重连 (已尝试 {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
"[MessageProcessor] 即将在 %s 秒后重连 (已尝试 %s/%s)",
backoff,
self.reconnect_count,
WSConfig.max_reconnect_attempts,
)
await asyncio.sleep(backoff)
else:
@@ -703,6 +708,7 @@ class MessageProcessor:
action_name = data.get("action_name", "")
task_id = data.get("task_id", "")
job_id = data.get("job_id", "")
notebook_id = data.get("notebook_id", "")
if not all([device_id, action_name, task_id, job_id]):
logger.error("[MessageProcessor] Missing required fields in query_action_state")
@@ -718,6 +724,7 @@ class MessageProcessor:
job_id=job_id,
task_id=task_id,
device_id=device_id,
notebook_id=notebook_id,
action_name=action_name,
device_action_key=device_action_key,
status=JobStatus.QUEUE,
@@ -732,13 +739,27 @@ class MessageProcessor:
if can_start_immediately:
# 可以立即开始
await self._send_action_state_response(
device_id, action_name, task_id, job_id, "query_action_status", True, 0
device_id,
action_name,
task_id,
job_id,
"query_action_status",
True,
0,
notebook_id=notebook_id,
)
logger.trace(f"[MessageProcessor] Job {job_log} can start immediately")
else:
# 需要排队
await self._send_action_state_response(
device_id, action_name, task_id, job_id, "query_action_status", False, 10
device_id,
action_name,
task_id,
job_id,
"query_action_status",
False,
10,
notebook_id=notebook_id,
)
logger.trace(f"[MessageProcessor] Job {job_log} queued")
@@ -768,6 +789,7 @@ class MessageProcessor:
job_id=req.job_id,
task_id=req.task_id,
device_id=req.device_id,
notebook_id=req.notebook_id,
action_name=action_name,
device_action_key=device_action_key,
status=JobStatus.QUEUE,
@@ -775,11 +797,16 @@ class MessageProcessor:
always_free=True,
)
self.device_manager.add_queue_request(job_info)
existing_job = job_info
logger.info(f"[MessageProcessor] Job {job_log} always_free, auto-registered from direct job_start")
else:
logger.error(f"[MessageProcessor] Job {job_log} not registered (missing query_action_state)")
return
if existing_job and req.notebook_id and not existing_job.notebook_id:
existing_job.notebook_id = req.notebook_id
notebook_id = req.notebook_id or (existing_job.notebook_id if existing_job else "")
success = self.device_manager.start_job(req.job_id)
if not success:
logger.error(f"[MessageProcessor] Failed to start job {job_log}")
@@ -795,6 +822,7 @@ class MessageProcessor:
action_name=req.action,
task_id=req.task_id,
job_id=req.job_id,
notebook_id=notebook_id,
device_action_key=device_action_key,
)
@@ -834,6 +862,7 @@ class MessageProcessor:
"job_id": req.job_id,
"task_id": req.task_id,
"device_id": req.device_id,
"notebook_id": queue_item.notebook_id,
"action_name": req.action,
"status": "failed",
"feedback_data": {},
@@ -855,6 +884,7 @@ class MessageProcessor:
"query_action_status",
True,
0,
notebook_id=next_job.notebook_id,
)
next_job_log = format_job_log(
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
@@ -1101,7 +1131,15 @@ class MessageProcessor:
logger.info(f"[MessageProcessor] Restart cleanup scheduled")
async def _send_action_state_response(
self, device_id: str, action_name: str, task_id: str, job_id: str, typ: str, free: bool, need_more: int
self,
device_id: str,
action_name: str,
task_id: str,
job_id: str,
typ: str,
free: bool,
need_more: int,
notebook_id: str = "",
):
"""发送动作状态响应"""
message = {
@@ -1112,6 +1150,7 @@ class MessageProcessor:
"action_name": action_name,
"task_id": task_id,
"job_id": job_id,
"notebook_id": notebook_id,
"free": free,
"need_more": need_more + 1,
},
@@ -1194,6 +1233,7 @@ class QueueProcessor:
action_name=timeout_job.action_name,
task_id=timeout_job.task_id,
job_id=timeout_job.job_id,
notebook_id=timeout_job.notebook_id,
device_action_key=timeout_job.device_action_key,
)
# 发布超时失败状态这会触发正常的job完成流程
@@ -1252,6 +1292,7 @@ class QueueProcessor:
"action_name": job_info.action_name,
"task_id": job_info.task_id,
"job_id": job_info.job_id,
"notebook_id": job_info.notebook_id,
"free": False,
"need_more": 10 + 1,
},
@@ -1291,6 +1332,7 @@ class QueueProcessor:
"action_name": job_info.action_name,
"task_id": job_info.task_id,
"job_id": job_info.job_id,
"notebook_id": job_info.notebook_id,
"free": False,
"need_more": 10 + 1,
},
@@ -1336,12 +1378,15 @@ class QueueProcessor:
"action_name": next_job.action_name,
"task_id": next_job.task_id,
"job_id": next_job.job_id,
"notebook_id": next_job.notebook_id,
"free": True,
"need_more": 0,
},
}
self.message_processor.send_message(message)
# next_job_log = format_job_log(next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name)
# next_job_log = format_job_log(
# next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
# )
# logger.debug(f"[QueueProcessor] Notified next job {next_job_log} can start")
# 立即触发下一轮状态检查
@@ -1510,6 +1555,7 @@ class WebSocketClient(BaseCommunicationClient):
"job_id": item.job_id,
"task_id": item.task_id,
"device_id": item.device_id,
"notebook_id": item.notebook_id,
"action_name": item.action_name,
"status": status,
"feedback_data": feedback_data,

View File

@@ -47,7 +47,10 @@ def _has_uv() -> bool:
def _install_command(installer: str, package: str, upgrade: bool, is_chinese: bool) -> List[str]:
if installer == "uv":
cmd = ["uv", "pip", "install"]
# uv >= 0.5 默认要求虚拟环境,对 conda env 会报 "No virtual environment found"。
# 显式 --python sys.executable 让 uv 把当前解释器conda/venv/system 都行)
# 视为目标环境,绕开 venv 检测。
cmd = ["uv", "pip", "install", "--python", sys.executable]
if upgrade:
cmd.append("--upgrade")
cmd.append(package)
@@ -89,7 +92,11 @@ def _print_manual_git_install_hint(requirement: str) -> None:
return
repo_dir = _repo_dir_name(git_url)
install_cmd = "uv pip install -e ." if _has_uv() else f"{sys.executable} -m pip install -e ."
install_cmd = (
f'uv pip install --python "{sys.executable}" -e .'
if _has_uv()
else f"{sys.executable} -m pip install -e ."
)
if _is_chinese_locale() and not _has_uv():
install_cmd += " -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"

View File

@@ -2,7 +2,7 @@
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>unilabos_msgs</name>
<version>0.11.1</version>
<version>0.11.2</version>
<description>ROS2 Messages package for unilabos devices</description>
<maintainer email="changjh@pku.edu.cn">Junhan Chang</maintainer>
<maintainer email="18435084+Xuwznln@users.noreply.github.com">Xuwznln</maintainer>