[CI/Build] Strengthen pre-commit with markdownlint, SPDX, and policy hooks (#6273)
Signed-off-by: wangyu <410167048@qq.com> Signed-off-by: [Your Name] <your.email@example.com> Signed-off-by: wangyu <53896905+yenuo26@users.noreply.github.com>
This commit is contained in:
1 parent
10906e3a6d
commit
273ffce130
30 files changed
+1497
-332
No files matched your search
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Validate .buildkite YAML against the official Buildkite schema.
|
||||
|
||||
Test pipelines (test-ready.yml, etc.) use Omni-only keys such as
|
||||
``mirror_hardwares`` and ``source_file_dependencies``. Those are expanded or
|
||||
stripped the same way ``upload_pipeline.py`` does before upload, then the
|
||||
rendered YAML is checked with check-jsonschema.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPTS = ROOT / ".buildkite" / "common" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
from upload_pipeline import _render_test_pipeline # noqa: E402
|
||||
|
||||
SKIP_FILES = {
|
||||
".buildkite/common/ci_mirror_hardwares.yml",
|
||||
".buildkite/npu/pipeline-npu-a3.yml",
|
||||
}
|
||||
|
||||
# AMD pipelines are uploaded by a different path and use list-valued
|
||||
# mirror_hardwares plus agent_pool/grade, which CUDA's expander rejects.
|
||||
AMD_STRIP_KEYS = frozenset({"mirror_hardwares", "agent_pool", "grade", "source_file_dependencies"})
|
||||
|
||||
|
||||
def _normalize(path: str) -> str:
|
||||
return path.replace("\\", "/")
|
||||
|
||||
|
||||
def _strip_keys(obj: Any, keys: frozenset[str]) -> Any:
|
||||
if isinstance(obj, dict):
|
||||
return {k: _strip_keys(v, keys) for k, v in obj.items() if k not in keys}
|
||||
if isinstance(obj, list):
|
||||
return [_strip_keys(item, keys) for item in obj]
|
||||
return obj
|
||||
|
||||
|
||||
def _prepare_pipeline(path: Path) -> dict[str, Any]:
|
||||
rel = _normalize(str(path.relative_to(ROOT) if path.is_absolute() else path))
|
||||
doc = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(doc, dict):
|
||||
raise ValueError(f"{rel}: expected a mapping (pipeline YAML), got {type(doc).__name__}")
|
||||
|
||||
if "/amd/" in f"/{rel}":
|
||||
return _strip_keys(doc, AMD_STRIP_KEYS)
|
||||
|
||||
return _render_test_pipeline(doc, changed_files=None)
|
||||
|
||||
|
||||
def _validate(path: Path, doc: dict[str, Any]) -> int:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
suffix=".yml",
|
||||
encoding="utf-8",
|
||||
delete=False,
|
||||
) as handle:
|
||||
yaml.safe_dump(doc, handle, sort_keys=False)
|
||||
tmp = Path(handle.name)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"check_jsonschema",
|
||||
"--builtin-schema",
|
||||
"vendor.buildkite",
|
||||
str(tmp),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
return 0
|
||||
|
||||
rel = _normalize(str(path))
|
||||
sys.stderr.write(f"{rel}: Buildkite schema validation failed\n")
|
||||
sys.stderr.write(result.stdout or "")
|
||||
sys.stderr.write(result.stderr or "")
|
||||
return 1
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
rc = 0
|
||||
for raw in argv:
|
||||
path = Path(raw)
|
||||
rel = _normalize(str(path))
|
||||
if rel in SKIP_FILES:
|
||||
continue
|
||||
if not path.is_file():
|
||||
path = ROOT / path
|
||||
try:
|
||||
doc = _prepare_pipeline(path)
|
||||
except Exception as exc: # noqa: BLE001 - surface expander errors as hook failures
|
||||
sys.stderr.write(f"{rel}: {exc}\n")
|
||||
rc = 1
|
||||
continue
|
||||
rc |= _validate(path, doc)
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,368 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Ban dangerous / policy-violating imports unless a file is allowlisted.
|
||||
|
||||
Ported from vllm/tools/pre_commit/check_forbidden_imports.py. Existing
|
||||
vllm_omni/ usages are grandfathered; add a path to the matching
|
||||
allowed_files set only after a security/policy review.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import regex as re
|
||||
|
||||
# Hub entry points that must go through the vLLM-tagged repo_utils helpers.
|
||||
_HF_NAMES = (
|
||||
r"HfApi|HfFileSystem|hf_hub_download|snapshot_download"
|
||||
r"|list_repo_files|file_exists|try_to_load_from_cache"
|
||||
r"|list_repo_refs|repo_exists"
|
||||
)
|
||||
|
||||
# Non-library trees: examples, tests, and tooling may keep stdlib imports.
|
||||
_NON_LIBRARY_DIRS = {
|
||||
"examples/",
|
||||
"tests/",
|
||||
"benchmarks/",
|
||||
"apps/",
|
||||
"docs/",
|
||||
"tools/",
|
||||
"scripts/",
|
||||
".buildkite/",
|
||||
".github/",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ForbiddenImport:
|
||||
pattern: str
|
||||
tip: str
|
||||
allowed_pattern: re.Pattern = re.compile(r"^$") # matches nothing by default
|
||||
allowed_files: set[str] = field(default_factory=set)
|
||||
allowed_dirs: set[str] = field(default_factory=set)
|
||||
|
||||
|
||||
CHECK_IMPORTS = {
|
||||
"pickle/cloudpickle": ForbiddenImport(
|
||||
pattern=(
|
||||
r"^\s*(import\s+(?:[\w.]+\s*(?:as\s+\w+)?\s*,\s*)*(pickle|cloudpickle)\b"
|
||||
r"|from\s+(pickle|cloudpickle)\s+import\b)"
|
||||
),
|
||||
tip=("Avoid using pickle or cloudpickle or add this file to tools/pre_commit/check_forbidden_imports.py."),
|
||||
allowed_files={
|
||||
# STOP AND READ BEFORE YOU ADD ANYTHING ELSE TO THIS LIST:
|
||||
# pickle/cloudpickle are unsafe when deserializing untrusted data.
|
||||
"tests/diffusion/attention/test_attention_sp.py",
|
||||
"tests/helpers/process.py",
|
||||
"vllm_omni/diffusion/distributed/group_coordinator.py",
|
||||
},
|
||||
),
|
||||
"base64": ForbiddenImport(
|
||||
pattern=r"^\s*(?:import\s+base64(?:$|\s|,)|from\s+base64\s+import)",
|
||||
tip=("Replace 'import base64' with 'import pybase64' or 'import pybase64 as base64'."),
|
||||
allowed_pattern=re.compile(r"^\s*import\s+pybase64(\s*|\s+as\s+base64\s*)$"),
|
||||
allowed_dirs=_NON_LIBRARY_DIRS,
|
||||
allowed_files={
|
||||
"vllm_omni/benchmarks/data_modules/daily_omni_dataset.py",
|
||||
"vllm_omni/benchmarks/data_modules/random_multi_modal_dataset.py",
|
||||
"vllm_omni/benchmarks/data_modules/seed_tts_dataset.py",
|
||||
"vllm_omni/entrypoints/openai/api_server.py",
|
||||
"vllm_omni/entrypoints/openai/audio_utils_mixin.py",
|
||||
"vllm_omni/entrypoints/openai/image_api_utils.py",
|
||||
"vllm_omni/entrypoints/openai/protocol/images.py",
|
||||
"vllm_omni/entrypoints/openai/realtime_connection.py",
|
||||
"vllm_omni/entrypoints/openai/serving_chat.py",
|
||||
"vllm_omni/entrypoints/openai/serving_speech.py",
|
||||
"vllm_omni/entrypoints/openai/serving_speech_stream.py",
|
||||
"vllm_omni/entrypoints/openai/video_api_utils.py",
|
||||
"vllm_omni/entrypoints/openai/video_stream_base.py",
|
||||
"vllm_omni/entrypoints/openai/video_stream_session.py",
|
||||
"vllm_omni/experimental/fullduplex/client.py",
|
||||
"vllm_omni/experimental/fullduplex/engine/contracts.py",
|
||||
"vllm_omni/experimental/fullduplex/joyvl/memory/memory.py",
|
||||
"vllm_omni/experimental/fullduplex/minicpmo45/adapter.py",
|
||||
"vllm_omni/experimental/fullduplex/minicpmo45/input.py",
|
||||
"vllm_omni/experimental/fullduplex/minicpmo45/runtime.py",
|
||||
"vllm_omni/experimental/fullduplex/minicpmo45/stage0.py",
|
||||
"vllm_omni/experimental/fullduplex/openai/audio.py",
|
||||
"vllm_omni/experimental/fullduplex/openai/realtime_input.py",
|
||||
"vllm_omni/experimental/fullduplex/openai/runtime_bridge.py",
|
||||
"vllm_omni/experimental/fullduplex/openai/serving.py",
|
||||
"vllm_omni/experimental/fullduplex/personaplex/input.py",
|
||||
"vllm_omni/experimental/fullduplex/personaplex/runtime_extension.py",
|
||||
"vllm_omni/experimental/fullduplex/personaplex/stage0.py",
|
||||
"vllm_omni/model_executor/models/indextts2/tokenizer_v2_5.py",
|
||||
"vllm_omni/model_executor/models/qwen3_tts/prompt_embeds_builder.py",
|
||||
"vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_tokenizer.py",
|
||||
"vllm_omni/tokenizers/mammoth_moda2_tokenizer.py",
|
||||
"vllm_omni/transformers_utils/processors/audiox.py",
|
||||
},
|
||||
),
|
||||
"re": ForbiddenImport(
|
||||
pattern=r"^\s*(?:import\s+re(?:$|\s|,)|from\s+re\s+import)",
|
||||
tip="Replace 'import re' with 'import regex as re' or 'import regex'.",
|
||||
allowed_pattern=re.compile(r"^\s*import\s+regex(\s*|\s+as\s+re\s*)$"),
|
||||
allowed_dirs=_NON_LIBRARY_DIRS,
|
||||
allowed_files={
|
||||
"setup.py",
|
||||
"vllm_omni/benchmarks/data_modules/daily_omni_eval.py",
|
||||
"vllm_omni/config/stage_config.py",
|
||||
"vllm_omni/diffusion/layers/mot/ops/mot_gemm.py",
|
||||
"vllm_omni/diffusion/model_loader/diffusers_loader.py",
|
||||
"vllm_omni/diffusion/models/diffusers_adapter/pipeline_diffusers_adapter.py",
|
||||
"vllm_omni/diffusion/models/dreamid_omni/fusion.py",
|
||||
"vllm_omni/diffusion/models/dreamzero/pipeline_dreamzero.py",
|
||||
"vllm_omni/diffusion/models/glm_image/pipeline_glm_image.py",
|
||||
"vllm_omni/diffusion/models/gr00t/modeling/processing_gr00t_n1d7.py",
|
||||
"vllm_omni/diffusion/models/hunyuan_video/pipeline_hunyuan_video_1_5.py",
|
||||
"vllm_omni/diffusion/models/longcat_image/pipeline_longcat_image.py",
|
||||
"vllm_omni/diffusion/models/longcat_image/pipeline_longcat_image_edit.py",
|
||||
"vllm_omni/diffusion/models/longcat_video/pipeline_longcat_video_avatar.py",
|
||||
"vllm_omni/diffusion/models/minimax_h3/encoder.py",
|
||||
"vllm_omni/diffusion/models/nextstep_1_1/pipeline_nextstep_1_1.py",
|
||||
"vllm_omni/diffusion/models/omnivoice/pipeline_omnivoice.py",
|
||||
"vllm_omni/diffusion/models/soulx_singer/modules/preprocess/asr.py",
|
||||
"vllm_omni/diffusion/models/soulx_singer/preprocess/g2p.py",
|
||||
"vllm_omni/diffusion/profiler/diffusion_pipeline_profiler.py",
|
||||
"vllm_omni/distributed/omni_connectors/utils/env.py",
|
||||
"vllm_omni/entrypoints/cli/logo.py",
|
||||
"vllm_omni/entrypoints/openai/serving_speech.py",
|
||||
"vllm_omni/experimental/fullduplex/joyvl/memory/memory.py",
|
||||
"vllm_omni/model_executor/models/common/whisper_vq.py",
|
||||
"vllm_omni/model_executor/models/fish_speech/prompt_utils.py",
|
||||
"vllm_omni/model_executor/models/glm_tts/text_frontend.py",
|
||||
"vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_tokenizer.py",
|
||||
"vllm_omni/model_executor/models/indextts2/indextts2_talker.py",
|
||||
"vllm_omni/model_executor/models/indextts2/text_processing_v2_5.py",
|
||||
"vllm_omni/model_executor/models/indextts2/utils/common.py",
|
||||
"vllm_omni/model_executor/models/ming_flash_omni/text_processing.py",
|
||||
"vllm_omni/model_executor/models/ming_flash_omni/vision_encoder.py",
|
||||
"vllm_omni/model_executor/models/ming_tts/prompt_assembly.py",
|
||||
"vllm_omni/model_executor/models/nemotron_voicechat/nemo_vendored/ear_tts_commons.py",
|
||||
"vllm_omni/model_executor/stage_input_processors/ming_flash_omni.py",
|
||||
"vllm_omni/outputs/output_modality.py",
|
||||
"vllm_omni/quantization/mxfp4_config.py",
|
||||
"vllm_omni/quantization/tools/merge_mxfp4_dualscale_checkpoint.py",
|
||||
"vllm_omni/utils/custom_voice_io.py",
|
||||
},
|
||||
),
|
||||
"triton": ForbiddenImport(
|
||||
pattern=r"^(from|import)\s+triton(\s|\.|$)",
|
||||
tip="Use 'from vllm.triton_utils import triton' instead.",
|
||||
allowed_pattern=re.compile(r"from vllm.triton_utils import (triton|tl|tl, triton)"),
|
||||
),
|
||||
"tilelang": ForbiddenImport(
|
||||
pattern=r"^(from|import)\s+tilelang(\s|\.|$)",
|
||||
tip="Use 'from vllm.tilelang_utils import tilelang, T' instead.",
|
||||
allowed_pattern=re.compile(
|
||||
r"from\s+vllm\.tilelang_utils\s+import\s+"
|
||||
r"(tilelang|T|T, tilelang|tilelang, T)\b"
|
||||
),
|
||||
),
|
||||
"huggingface_hub repo API": ForbiddenImport(
|
||||
# Catch `from huggingface_hub import `, including parenthesized,
|
||||
# multi-line imports.
|
||||
pattern=(
|
||||
r"^\s*from\s+huggingface_hub\s+import\s*\([^)]*\b(?:" + _HF_NAMES + r")\b"
|
||||
r"|"
|
||||
r"^\s*from\s+huggingface_hub\s+import\b[^\n]*\b(?:" + _HF_NAMES + r")\b"
|
||||
),
|
||||
tip=(
|
||||
"Use the shared, vLLM-tagged helpers from "
|
||||
"vllm.transformers_utils.repo_utils (e.g. hf_api(), hf_fs(), "
|
||||
"list_repo_files, file_exists) instead of calling "
|
||||
"huggingface_hub directly."
|
||||
),
|
||||
allowed_dirs=_NON_LIBRARY_DIRS,
|
||||
allowed_files={
|
||||
"vllm_omni/benchmarks/data_modules/daily_omni_dataset.py",
|
||||
"vllm_omni/benchmarks/data_modules/seed_tts_dataset.py",
|
||||
"vllm_omni/benchmarks/data_modules/seed_tts_eval.py",
|
||||
"vllm_omni/diffusion/lora/loader.py",
|
||||
"vllm_omni/diffusion/model_loader/hub_prefetch.py",
|
||||
"vllm_omni/diffusion/models/cosmos3/sound_tokenizer.py",
|
||||
"vllm_omni/diffusion/models/dreamzero/pipeline_dreamzero.py",
|
||||
"vllm_omni/diffusion/models/helios/pipeline_helios.py",
|
||||
"vllm_omni/diffusion/models/longcat_video/pipeline_longcat_video_avatar.py",
|
||||
"vllm_omni/diffusion/models/ltx2/ltx2_components.py",
|
||||
"vllm_omni/diffusion/models/magi_human/pipeline_magi_human.py",
|
||||
"vllm_omni/diffusion/models/omnivoice/pipeline_omnivoice.py",
|
||||
"vllm_omni/diffusion/models/pi0/pipeline_pi0.py",
|
||||
"vllm_omni/diffusion/models/sensenova_u1/pipeline_sensenova_u1.py",
|
||||
"vllm_omni/diffusion/models/soulx_singer/utils.py",
|
||||
"vllm_omni/diffusion/models/utils.py",
|
||||
"vllm_omni/diffusion/models/wan2_2/pipeline_wan2_2.py",
|
||||
"vllm_omni/engine/arg_utils.py",
|
||||
"vllm_omni/engine/stage_init_utils.py",
|
||||
"vllm_omni/entrypoints/openai/api_server.py",
|
||||
"vllm_omni/entrypoints/openai/serving_speech.py",
|
||||
"vllm_omni/entrypoints/openai/tts_adapters/cosyvoice3.py",
|
||||
"vllm_omni/experimental/fullduplex/personaplex/runtime.py",
|
||||
"vllm_omni/experimental/fullduplex/personaplex/serving/server.py",
|
||||
"vllm_omni/model_executor/model_loader/weight_utils.py",
|
||||
"vllm_omni/model_executor/models/audex/checkpoint.py",
|
||||
"vllm_omni/model_executor/models/bagel/bagel.py",
|
||||
"vllm_omni/model_executor/models/cosyvoice3/cosyvoice3.py",
|
||||
"vllm_omni/model_executor/models/covo_audio/covo_audio_code2wav.py",
|
||||
"vllm_omni/model_executor/models/dots_tts/dots_tts_talker.py",
|
||||
"vllm_omni/model_executor/models/dynin_omni/dynin_omni_common.py",
|
||||
"vllm_omni/model_executor/models/dynin_omni/dynin_omni_token2audio.py",
|
||||
"vllm_omni/model_executor/models/glm_tts/glm_tts.py",
|
||||
"vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_code2wav.py",
|
||||
"vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_tokenizer.py",
|
||||
"vllm_omni/model_executor/models/higgs_audio_v3/higgs_audio_v3_code2wav.py",
|
||||
"vllm_omni/model_executor/models/indextts2/preprocess_utils.py",
|
||||
"vllm_omni/model_executor/models/indextts2/s2mel/modules/bigvgan.py",
|
||||
"vllm_omni/model_executor/models/indextts2/tokenizer_v2_5.py",
|
||||
"vllm_omni/model_executor/models/ming_tts/speaker_extractor.py",
|
||||
"vllm_omni/model_executor/models/minicpmo_4_5/minicpmo_4_5_code2wav.py",
|
||||
"vllm_omni/model_executor/models/omnivoice/omnivoice.py",
|
||||
"vllm_omni/model_executor/models/personaplex/personaplex_mimi.py",
|
||||
"vllm_omni/model_executor/models/step_audio2/step_audio2_token2wav.py",
|
||||
"vllm_omni/model_executor/models/voxtral_tts/voxtral_tts.py",
|
||||
"vllm_omni/transformers_utils/configs/higgs_audio_v3.py",
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _normalize_path(path: str) -> str:
|
||||
return path.replace("\\", "/")
|
||||
|
||||
|
||||
def check_file(path: str) -> int:
|
||||
path = _normalize_path(path)
|
||||
with open(path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
return_code = 0
|
||||
# Check all patterns in the whole file
|
||||
for import_name, forbidden_import in CHECK_IMPORTS.items():
|
||||
# Skip files that are allowed for this import
|
||||
if path in forbidden_import.allowed_files:
|
||||
continue
|
||||
# Skip directories that are allowed for this import
|
||||
if any(path.startswith(prefix) for prefix in forbidden_import.allowed_dirs):
|
||||
continue
|
||||
# Search for forbidden imports
|
||||
for match in re.finditer(forbidden_import.pattern, content, re.MULTILINE):
|
||||
# Check if it's allowed
|
||||
if forbidden_import.allowed_pattern.match(match.group()):
|
||||
continue
|
||||
# Skip matches inside a comment
|
||||
line_start = content.rfind("\n", 0, match.start()) + 1
|
||||
if "#" in content[line_start : match.start()]:
|
||||
continue
|
||||
# Calculate line number from match position
|
||||
line_num = content[: match.start() + 1].count("\n") + 1
|
||||
print(
|
||||
f"{path}:{line_num}: "
|
||||
"\033[91merror:\033[0m " # red color
|
||||
f"Found forbidden import: {import_name}. {forbidden_import.tip}"
|
||||
)
|
||||
return_code = 1
|
||||
return return_code
|
||||
|
||||
|
||||
def main():
|
||||
returncode = 0
|
||||
for path in sys.argv[1:]:
|
||||
returncode |= check_file(path)
|
||||
return returncode
|
||||
|
||||
|
||||
def test_regex():
|
||||
def matches(rule: str, content: str) -> bool:
|
||||
return bool(re.search(CHECK_IMPORTS[rule].pattern, content, re.MULTILINE))
|
||||
|
||||
pickle_cases = [
|
||||
# Should match
|
||||
("import pickle", True),
|
||||
("import cloudpickle", True),
|
||||
("import pickle as pkl", True),
|
||||
("import cloudpickle as cpkl", True),
|
||||
("from pickle import *", True),
|
||||
("from cloudpickle import dumps", True),
|
||||
("from pickle import dumps, loads", True),
|
||||
("from cloudpickle import (dumps, loads)", True),
|
||||
(" import pickle", True),
|
||||
("\timport cloudpickle", True),
|
||||
("from pickle import loads", True),
|
||||
("import pickle, os", True),
|
||||
("import os, pickle", True),
|
||||
("import os, pickle as pkl", True),
|
||||
("import pickle as pkl, os", True),
|
||||
("import os.path, cloudpickle", True),
|
||||
("import cloudpickle, json", True),
|
||||
# Should not match
|
||||
("import somethingelse", False),
|
||||
("from somethingelse import pickle", False),
|
||||
("# import pickle", False),
|
||||
("print('import pickle')", False),
|
||||
("import pickleas as asdf", False),
|
||||
]
|
||||
for i, (content, should_match) in enumerate(pickle_cases):
|
||||
result = matches("pickle/cloudpickle", content)
|
||||
assert result == should_match, f"pickle case {i} failed: {content!r} (expected {should_match}, got {result})"
|
||||
|
||||
tilelang_cases = [
|
||||
# Should match
|
||||
("import tilelang", True),
|
||||
("import tilelang.language as T", True),
|
||||
("from tilelang.jit import JITImpl", True),
|
||||
("from tilelang.jit.kernel import JITKernel", True),
|
||||
# Should not match: indented (local) imports are allowed, mirroring
|
||||
# the "triton" rule, so mocked-module test imports are not flagged.
|
||||
(" import tilelang", False),
|
||||
(" from tilelang.jit import JITImpl", False),
|
||||
("from vllm.tilelang_utils import tilelang", False),
|
||||
("from vllm.tilelang_utils import T", False),
|
||||
("from vllm.tilelang_utils import T, tilelang", False),
|
||||
("from vllm.tilelang_utils import tilelang, T", False),
|
||||
("import tilelang_kernels", False),
|
||||
]
|
||||
for i, (content, should_match) in enumerate(tilelang_cases):
|
||||
result = matches("tilelang", content)
|
||||
assert result == should_match, f"tilelang case {i} failed: {content!r} (expected {should_match}, got {result})"
|
||||
|
||||
hf_cases = [
|
||||
# Should match
|
||||
("from huggingface_hub import snapshot_download", True),
|
||||
("from huggingface_hub import hf_hub_download", True),
|
||||
("from huggingface_hub import HfApi", True),
|
||||
("from huggingface_hub import HfFileSystem", True),
|
||||
("from huggingface_hub import list_repo_files", True),
|
||||
("from huggingface_hub import try_to_load_from_cache", True),
|
||||
(" from huggingface_hub import snapshot_download", True),
|
||||
("from huggingface_hub import PyTorchModelHubMixin, hf_hub_download", True),
|
||||
("from huggingface_hub import (snapshot_download)", True),
|
||||
# Parenthesized multi-line import must not bypass the hook
|
||||
("from huggingface_hub import (\n snapshot_download,\n)", True),
|
||||
(
|
||||
"from huggingface_hub import (\n PyTorchModelHubMixin,\n HfApi,\n)",
|
||||
True,
|
||||
),
|
||||
# Should not match
|
||||
("import huggingface_hub", False),
|
||||
("import huggingface_hub as hf", False),
|
||||
("from huggingface_hub import PyTorchModelHubMixin", False),
|
||||
("from huggingface_hub.constants import HF_HUB_CACHE", False),
|
||||
("from huggingface_hub.utils import EntryNotFoundError", False),
|
||||
("from vllm.transformers_utils.repo_utils import hf_api", False),
|
||||
("from huggingface_hub import (\n PyTorchModelHubMixin,\n)", False),
|
||||
("# from huggingface_hub import snapshot_download", False),
|
||||
]
|
||||
for i, (content, should_match) in enumerate(hf_cases):
|
||||
result = matches("huggingface_hub repo API", content)
|
||||
assert result == should_match, (
|
||||
f"huggingface_hub case {i} failed: {content!r} (expected {should_match}, got {result})"
|
||||
)
|
||||
|
||||
print("All regex tests passed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "--test-regex" in sys.argv:
|
||||
test_regex()
|
||||
else:
|
||||
sys.exit(main())
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import sys
|
||||
|
||||
import regex as re
|
||||
|
||||
# List of files (relative to repo root) that are allowed to import pickle or
|
||||
# cloudpickle
|
||||
#
|
||||
# STOP AND READ BEFORE YOU ADD ANYTHING ELSE TO THIS LIST:
|
||||
# The pickle and cloudpickle modules are known to be unsafe when deserializing
|
||||
# data from potentially untrusted parties. They have resulted in multiple CVEs
|
||||
# for vLLM and numerous vulnerabilities in the Python ecosystem more broadly.
|
||||
# Before adding new uses of pickle/cloudpickle, please consider safer
|
||||
# alternatives like msgpack or pydantic that are already in use in vLLM. Only
|
||||
# add to this list if absolutely necessary and after careful security review.
|
||||
ALLOWED_FILES = {
|
||||
"tests/helpers/process.py",
|
||||
"vllm_omni/diffusion/distributed/group_coordinator.py",
|
||||
"tests/diffusion/attention/test_attention_sp.py",
|
||||
# Measures the wire size of diffusion outputs. The transport pickles them,
|
||||
# and only pickle reproduces the per-view storage duplication under test
|
||||
# (torch.save deduplicates shared storages). No untrusted data is loaded.
|
||||
"tests/diffusion/test_diffusion_ipc.py",
|
||||
}
|
||||
|
||||
PICKLE_RE = re.compile(
|
||||
r"^\s*(import\s+(pickle|cloudpickle)(\s|$|\sas)"
|
||||
r"|from\s+(pickle|cloudpickle)\s+import\b)"
|
||||
)
|
||||
|
||||
|
||||
def scan_file(path: str) -> int:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for i, line in enumerate(f, 1):
|
||||
if PICKLE_RE.match(line):
|
||||
print(
|
||||
f"{path}:{i}: "
|
||||
"\033[91merror:\033[0m " # red color
|
||||
"Found pickle/cloudpickle import"
|
||||
)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
returncode = 0
|
||||
for filename in sys.argv[1:]:
|
||||
if filename in ALLOWED_FILES:
|
||||
continue
|
||||
returncode |= scan_file(filename)
|
||||
return returncode
|
||||
|
||||
|
||||
def test_regex():
|
||||
test_cases = [
|
||||
# Should match
|
||||
("import pickle", True),
|
||||
("import cloudpickle", True),
|
||||
("import pickle as pkl", True),
|
||||
("import cloudpickle as cpkl", True),
|
||||
("from pickle import *", True),
|
||||
("from cloudpickle import dumps", True),
|
||||
("from pickle import dumps, loads", True),
|
||||
("from cloudpickle import (dumps, loads)", True),
|
||||
(" import pickle", True),
|
||||
("\timport cloudpickle", True),
|
||||
("from pickle import loads", True),
|
||||
# Should not match
|
||||
("import somethingelse", False),
|
||||
("from somethingelse import pickle", False),
|
||||
("# import pickle", False),
|
||||
("print('import pickle')", False),
|
||||
("import pickleas as asdf", False),
|
||||
]
|
||||
for i, (line, should_match) in enumerate(test_cases):
|
||||
result = bool(PICKLE_RE.match(line))
|
||||
assert result == should_match, f"Test case {i} failed: '{line}' (expected {should_match}, got {result})"
|
||||
print("All regex tests passed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "--test-regex" in sys.argv:
|
||||
test_regex()
|
||||
else:
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Require SPDX license headers on source files.
|
||||
|
||||
Ported from vllm/tools/pre_commit/check_spdx_header.py. The copyright line
|
||||
uses the vLLM-Omni project name rather than upstream vLLM. Shell scripts
|
||||
(``.sh``) are included so wrappers such as ``shellcheck.sh`` cannot keep a
|
||||
stale upstream copyright that Python/Rust/proto files would have rewritten.
|
||||
``.pyi`` stubs use the same ``#`` header style as ``.py``.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
# Older files still carry the upstream vLLM copyright line. Treat it as stale
|
||||
# and rewrite it to the Omni line instead of inserting a duplicate header.
|
||||
LEGACY_COPYRIGHT_TEXT = "SPDX-FileCopyrightText: Copyright contributors to the vLLM project"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HeaderStyle:
|
||||
"""Comment syntax and preamble handling for an SPDX header."""
|
||||
|
||||
comment_prefix: str
|
||||
preserve_shebang: bool = False
|
||||
|
||||
|
||||
class SPDXStatus(Enum):
|
||||
"""SPDX header status enumeration"""
|
||||
|
||||
EMPTY = "empty" # empty __init__.py
|
||||
COMPLETE = "complete"
|
||||
MISSING_LICENSE = "missing_license" # Only has copyright line
|
||||
MISSING_COPYRIGHT = "missing_copyright" # Only has license line
|
||||
MISSING_BOTH = "missing_both" # Completely missing
|
||||
|
||||
|
||||
LICENSE_TEXT = "SPDX-License-Identifier: Apache-2.0"
|
||||
COPYRIGHT_TEXT = "SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project"
|
||||
FILE_STYLES = {
|
||||
".py": HeaderStyle("#", preserve_shebang=True),
|
||||
".pyi": HeaderStyle("#"),
|
||||
".sh": HeaderStyle("#", preserve_shebang=True),
|
||||
".rs": HeaderStyle("//"),
|
||||
".proto": HeaderStyle("//"),
|
||||
}
|
||||
|
||||
|
||||
def file_style(file_path):
|
||||
"""Return the declared header style for a file."""
|
||||
suffix = Path(file_path).suffix
|
||||
try:
|
||||
return FILE_STYLES[suffix]
|
||||
except KeyError:
|
||||
raise ValueError(f"Unsupported file type: {file_path}") from None
|
||||
|
||||
|
||||
def spdx_header(style):
|
||||
"""Return the SPDX header for a file style."""
|
||||
license_line = f"{style.comment_prefix} {LICENSE_TEXT}"
|
||||
copyright_line = f"{style.comment_prefix} {COPYRIGHT_TEXT}"
|
||||
return license_line, copyright_line
|
||||
|
||||
|
||||
def legacy_copyright_line(style):
|
||||
"""Return the stale upstream vLLM copyright line for a file style."""
|
||||
return f"{style.comment_prefix} {LEGACY_COPYRIGHT_TEXT}"
|
||||
|
||||
|
||||
def header_insertion_index(style, lines):
|
||||
"""Return the line index where a missing header should be inserted."""
|
||||
if style.preserve_shebang and lines and lines[0].startswith("#!"):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def check_spdx_header_status(file_path):
|
||||
"""Check SPDX header status of the file"""
|
||||
style = file_style(file_path)
|
||||
license_line, copyright_line = spdx_header(style)
|
||||
with open(file_path, encoding="UTF-8") as file:
|
||||
lines = file.readlines()
|
||||
if not lines:
|
||||
# Empty file
|
||||
return SPDXStatus.EMPTY
|
||||
|
||||
has_license = False
|
||||
has_copyright = False
|
||||
|
||||
# Check all lines for SPDX headers (not just the first two)
|
||||
for raw_line in lines:
|
||||
line = raw_line.strip()
|
||||
if line == license_line:
|
||||
has_license = True
|
||||
elif line == copyright_line:
|
||||
has_copyright = True
|
||||
|
||||
# Determine status based on what we found
|
||||
if has_license and has_copyright:
|
||||
return SPDXStatus.COMPLETE
|
||||
elif has_license and not has_copyright:
|
||||
# Only has license line (or a stale vLLM copyright line)
|
||||
return SPDXStatus.MISSING_COPYRIGHT
|
||||
elif not has_license and has_copyright:
|
||||
# Only has copyright line
|
||||
return SPDXStatus.MISSING_LICENSE
|
||||
else:
|
||||
# Completely missing both lines
|
||||
return SPDXStatus.MISSING_BOTH
|
||||
|
||||
|
||||
def add_header(file_path, status):
|
||||
"""Add or supplement SPDX header based on status"""
|
||||
style = file_style(file_path)
|
||||
license_line, copyright_line = spdx_header(style)
|
||||
stale_copyright_line = legacy_copyright_line(style)
|
||||
full_spdx_header = f"{license_line}\n{copyright_line}"
|
||||
with open(file_path, "r+", encoding="UTF-8") as file:
|
||||
lines = file.readlines()
|
||||
file.seek(0, 0)
|
||||
file.truncate()
|
||||
|
||||
if status == SPDXStatus.MISSING_BOTH:
|
||||
# Completely missing, add complete header
|
||||
insertion_index = header_insertion_index(style, lines)
|
||||
file.writelines(lines[:insertion_index])
|
||||
file.write(full_spdx_header + "\n")
|
||||
remaining_lines = lines[insertion_index:]
|
||||
if remaining_lines and remaining_lines[0].strip():
|
||||
file.write("\n")
|
||||
file.writelines(remaining_lines)
|
||||
|
||||
elif status == SPDXStatus.MISSING_COPYRIGHT:
|
||||
# Only has license line, need to add copyright line.
|
||||
# Rewrite a stale upstream vLLM copyright line in place.
|
||||
replaced = False
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip() == stale_copyright_line:
|
||||
lines[i] = f"{copyright_line}\n"
|
||||
replaced = True
|
||||
break
|
||||
if not replaced:
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip() == license_line:
|
||||
lines.insert(i + 1, f"{copyright_line}\n")
|
||||
break
|
||||
|
||||
file.writelines(lines)
|
||||
|
||||
elif status == SPDXStatus.MISSING_LICENSE:
|
||||
# Only has copyright line, need to add license line
|
||||
# Find the copyright line and add license line before it
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip() == copyright_line:
|
||||
# Insert license line before copyright line
|
||||
lines.insert(i, f"{license_line}\n")
|
||||
break
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
files_missing_both = []
|
||||
files_missing_copyright = []
|
||||
files_missing_license = []
|
||||
|
||||
for file_path in sys.argv[1:]:
|
||||
if Path(file_path).suffix not in FILE_STYLES:
|
||||
continue
|
||||
status = check_spdx_header_status(file_path)
|
||||
|
||||
if status == SPDXStatus.MISSING_BOTH:
|
||||
files_missing_both.append(file_path)
|
||||
elif status == SPDXStatus.MISSING_COPYRIGHT:
|
||||
files_missing_copyright.append(file_path)
|
||||
elif status == SPDXStatus.MISSING_LICENSE:
|
||||
files_missing_license.append(file_path)
|
||||
else:
|
||||
continue
|
||||
|
||||
# Collect all files that need fixing
|
||||
all_files_to_fix = files_missing_both + files_missing_copyright + files_missing_license
|
||||
if all_files_to_fix:
|
||||
print("The following files are missing the SPDX header:")
|
||||
if files_missing_both:
|
||||
for file_path in files_missing_both:
|
||||
print(f" {file_path}")
|
||||
add_header(file_path, SPDXStatus.MISSING_BOTH)
|
||||
|
||||
if files_missing_copyright:
|
||||
for file_path in files_missing_copyright:
|
||||
print(f" {file_path}")
|
||||
add_header(file_path, SPDXStatus.MISSING_COPYRIGHT)
|
||||
if files_missing_license:
|
||||
for file_path in files_missing_license:
|
||||
print(f" {file_path}")
|
||||
add_header(file_path, SPDXStatus.MISSING_LICENSE)
|
||||
|
||||
sys.exit(1 if all_files_to_fix else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Ban direct torch.cuda.* usage outside an allowlist.
|
||||
|
||||
Modeled on vllm/tools/pre_commit/check_torch_cuda.py. Call sites should use
|
||||
current_omni_platform / OmniPlatform instead of CUDA-only torch.cuda APIs.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import regex as re
|
||||
|
||||
# Match `torch.cuda.xxx` device/memory/seed helpers that have portable Omni
|
||||
# platform equivalents. CUDAGraph / is_available / stream capture are not in
|
||||
# this list (same split as upstream vLLM).
|
||||
_TORCH_CUDA_PATTERNS = [
|
||||
r"\btorch\.cuda\.(empty_cache|synchronize|device_count|current_device|memory_reserved|memory_allocated|max_memory_allocated|max_memory_reserved|reset_peak_memory_stats|memory_stats|mem_get_info|set_device)\b",
|
||||
# Match torch.cuda.device(...) including args. `device\()\b` misses
|
||||
# `device()` (no word char after `)`) and `device(0)`.
|
||||
r"\btorch\.cuda\.device\s*\(",
|
||||
r"\btorch\.cuda\.(manual_seed|manual_seed_all)\b",
|
||||
r"\bwith\storch\.cuda\.device\b",
|
||||
# Calls torch.cuda.{_is_compiled/_device_count_amdsmi/_device_count_nvml} internally
|
||||
r"\bcuda_device_count_stateless\(\)\b",
|
||||
]
|
||||
|
||||
# Platform adapters must call native torch.cuda / torch.npu / torch.xpu APIs.
|
||||
ALLOWED_PREFIXES = {
|
||||
"vllm_omni/platforms/",
|
||||
}
|
||||
|
||||
# Grandfathered call sites. Remove a path after migrating it to
|
||||
# current_omni_platform / OmniPlatform. Do not add new files without review.
|
||||
ALLOWED_FILES = {
|
||||
"benchmarks/kernels/mot_linear_benchmarks.py",
|
||||
"examples/online_serving/qwen2_5_omni/gradio_demo.py",
|
||||
"examples/online_serving/qwen3_omni/gradio_demo.py",
|
||||
"tests/dfx/reliability/helpers.py",
|
||||
"tests/diffusion/distributed/test_cfg_parallel.py",
|
||||
"tests/diffusion/distributed/test_pipeline_parallel.py",
|
||||
"tests/e2e/accuracy/wan22_i2v/run_wan22_i2v_diffusers_cp.py",
|
||||
"tests/e2e/features/custom_pipeline/test_async_omni_collective_rpc.py",
|
||||
"tests/e2e/features/fullduplex/test_personaplex_elastic.py",
|
||||
"tests/entrypoints/test_omni_sleep_mode.py",
|
||||
"tools/configure_stage_memory.py",
|
||||
"vllm_omni/diffusion/models/bagel/pipeline_bagel.py",
|
||||
"vllm_omni/diffusion/models/lance/pipeline_lance.py",
|
||||
"vllm_omni/diffusion/models/magi_human/pipeline_magi_human.py",
|
||||
"vllm_omni/diffusion/models/nextstep_1_1/pipeline_nextstep_1_1.py",
|
||||
"vllm_omni/distributed/omni_connectors/connectors/mooncake_transfer_engine_connector.py",
|
||||
"vllm_omni/distributed/omni_connectors/connectors/mori_transfer_engine_connector.py",
|
||||
"vllm_omni/experimental/ar_diffusion/runner.py",
|
||||
"vllm_omni/model_executor/models/moss_tts/cuda_graph_streaming_decoder_wrapper.py",
|
||||
"vllm_omni/model_executor/models/moss_tts_nano/modeling_moss_tts_nano.py",
|
||||
"vllm_omni/model_executor/models/qwen3_tts/cuda_graph_decoder_wrapper.py",
|
||||
"vllm_omni/worker/gpu_ar_model_runner.py",
|
||||
}
|
||||
|
||||
_GENERIC_TIP = (
|
||||
"Found torch.cuda API call. Use current_omni_platform / OmniPlatform "
|
||||
"(empty_cache, synchronize, get_device_count, current_device, "
|
||||
"get_device_memory, set_device) instead."
|
||||
)
|
||||
_SEED_TIP = (
|
||||
"Found CUDA manual-seed API call. Use "
|
||||
"torch.Generator(device=current_omni_platform.device_type).manual_seed(...) "
|
||||
"instead."
|
||||
)
|
||||
|
||||
|
||||
def _normalize_path(path: str) -> str:
|
||||
return path.replace("\\", "/")
|
||||
|
||||
|
||||
def _is_allowed(path: str) -> bool:
|
||||
if any(path.startswith(prefix) for prefix in ALLOWED_PREFIXES):
|
||||
return True
|
||||
return path in ALLOWED_FILES
|
||||
|
||||
|
||||
def scan_file(path: str) -> int:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
for pattern in _TORCH_CUDA_PATTERNS:
|
||||
for match in re.finditer(pattern, content, re.MULTILINE):
|
||||
line_start = content.rfind("\n", 0, match.start()) + 1
|
||||
if "#" in content[line_start : match.start()]:
|
||||
continue
|
||||
line_num = content[: match.start() + 1].count("\n") + 1
|
||||
matched_text = match.group(0)
|
||||
tip = _SEED_TIP if "manual_seed" in matched_text else _GENERIC_TIP
|
||||
print(f"{path}:{line_num}: \033[91merror:\033[0m {tip}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
returncode = 0
|
||||
for filename in sys.argv[1:]:
|
||||
path = _normalize_path(filename)
|
||||
if _is_allowed(path):
|
||||
continue
|
||||
returncode |= scan_file(path)
|
||||
return returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Run mypy on changed files.
|
||||
|
||||
Ported from vllm/tools/pre_commit/mypy.py. Group files so tests can use
|
||||
``follow_imports=skip`` without pulling that setting into library code.
|
||||
|
||||
Usage:
|
||||
python tools/pre_commit/mypy.py <python_version> <changed_files...>
|
||||
|
||||
Args:
|
||||
python_version: Python version to use (e.g. "3.10") or "local" to use
|
||||
the interpreter running this script.
|
||||
changed_files: List of changed files to check.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import regex as re
|
||||
|
||||
# tests/ uses follow_imports=skip so third-party and library errors do not
|
||||
# leak into test type-checking. After a directory is clean under skip, it
|
||||
# can join the default group (follow_imports from pyproject.toml).
|
||||
SEPARATE_GROUPS = [
|
||||
"tests",
|
||||
]
|
||||
|
||||
# Model trees are large third-party ports. Drop a prefix from this list when
|
||||
# that directory is ready to be type-checked (same ratchet as upstream vLLM).
|
||||
EXCLUDE = [
|
||||
r"vllm_omni/model_executor/models/",
|
||||
r"vllm_omni/diffusion/models/",
|
||||
]
|
||||
|
||||
|
||||
def _normalize_path(path: str) -> str:
|
||||
return path.replace("\\", "/")
|
||||
|
||||
|
||||
def group_files(changed_files: list[str]) -> dict[str, list[str]]:
|
||||
"""Group changed files into different mypy calls."""
|
||||
exclude_pattern = re.compile(f"^{'|'.join(EXCLUDE)}.*")
|
||||
file_groups: dict[str, list[str]] = {"": []}
|
||||
file_groups.update({k: [] for k in SEPARATE_GROUPS})
|
||||
# Longest path first so a sub-directory is not shadowed by its parent.
|
||||
separate_groups = sorted(SEPARATE_GROUPS, key=len, reverse=True)
|
||||
for changed_file in changed_files:
|
||||
changed_file = _normalize_path(changed_file)
|
||||
if exclude_pattern.match(changed_file):
|
||||
continue
|
||||
for directory in separate_groups:
|
||||
if re.match(f"^{directory}.*", changed_file):
|
||||
file_groups[directory].append(changed_file)
|
||||
break
|
||||
else:
|
||||
if changed_file.startswith(("vllm_omni/", "tests/")):
|
||||
file_groups[""].append(changed_file)
|
||||
return file_groups
|
||||
|
||||
|
||||
def mypy(
|
||||
targets: list[str],
|
||||
python_version: str | None,
|
||||
follow_imports: str | None,
|
||||
file_group: str,
|
||||
) -> int:
|
||||
"""Run mypy on the given targets."""
|
||||
args = ["mypy"]
|
||||
if python_version is not None:
|
||||
args += ["--python-version", python_version]
|
||||
if follow_imports is not None:
|
||||
args += ["--follow-imports", follow_imports]
|
||||
print(f"$ {' '.join(args)} {file_group}")
|
||||
return subprocess.run(args + targets, check=False).returncode
|
||||
|
||||
|
||||
def main() -> int:
|
||||
python_version = sys.argv[1]
|
||||
file_groups = group_files(sys.argv[2:])
|
||||
|
||||
if python_version == "local":
|
||||
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
|
||||
returncode = 0
|
||||
for file_group, changed_files in file_groups.items():
|
||||
follow_imports = None if file_group == "" else "skip"
|
||||
if changed_files:
|
||||
returncode |= mypy(changed_files, python_version, follow_imports, file_group)
|
||||
return returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
#
|
||||
# Lint bash scripts for undefined vars, quoting, and similar bugs.
|
||||
# Do not download a binary: use a distro/package-manager install so the
|
||||
# artifact is signed (apt/dnf/brew) or at least an explicit local install.
|
||||
set -euo pipefail
|
||||
|
||||
is_windows_exe() {
|
||||
[[ "$1" == *.exe ]]
|
||||
}
|
||||
|
||||
is_git_bash() {
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
find_native_shellcheck() {
|
||||
local cand
|
||||
cand="$(command -v shellcheck 2>/dev/null || true)"
|
||||
if [ -n "$cand" ] && ! is_windows_exe "$cand"; then
|
||||
echo "$cand"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
install_hint() {
|
||||
echo "Please install shellcheck with your package manager, then re-run:"
|
||||
echo " Debian/Ubuntu/WSL: sudo apt-get install shellcheck"
|
||||
echo " Fedora: sudo dnf install ShellCheck"
|
||||
echo " macOS: brew install shellcheck"
|
||||
echo " Git Bash: scoop install shellcheck"
|
||||
echo " https://github.com/koalaman/shellcheck?tab=readme-ov-file#installing"
|
||||
}
|
||||
|
||||
SHELLCHECK_BIN=""
|
||||
if SHELLCHECK_BIN="$(find_native_shellcheck)"; then
|
||||
:
|
||||
elif is_git_bash && SHELLCHECK_BIN="$(command -v shellcheck.exe 2>/dev/null || true)" && [ -n "$SHELLCHECK_BIN" ]; then
|
||||
:
|
||||
else
|
||||
install_hint
|
||||
exit 1
|
||||
fi
|
||||
|
||||
should_lint() {
|
||||
local f="${1//\\//}"
|
||||
f="${f#./}"
|
||||
case "$f" in
|
||||
*.sh) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
git check-ignore -q "$f" && return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
run_shellcheck() {
|
||||
local f
|
||||
for f in "$@"; do
|
||||
if should_lint "$f"; then
|
||||
"$SHELLCHECK_BIN" -s bash "$f"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
run_shellcheck "$@"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Direct invocation with no args: lint every tracked *.sh.
|
||||
while IFS= read -r -d '' f || [ -n "$f" ]; do
|
||||
git check-ignore -q "$f" && continue
|
||||
"$SHELLCHECK_BIN" -s bash "$f"
|
||||
done < <(find . -path ./.git -prune -o -name "*.sh" -print0)
|
||||
Reference in new issue
Block a user