Skip to content

Commit 85a0c2d

Browse files
committed
version and library type detection fixes
1 parent 7058e68 commit 85a0c2d

5 files changed

Lines changed: 300 additions & 7 deletions

File tree

cli/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,14 @@ def main(
781781
config.library_id = CppHandler.suggest_library_id_static(lib)
782782
elif language == Language.FORTRAN:
783783
config.library_id = FortranHandler.suggest_library_id_static(lib)
784+
785+
# Normalize versions by checking git tags
786+
click.echo("Checking git tags for version format...")
787+
config.normalize_versions_with_git_lookup()
788+
if config.target_prefix:
789+
click.echo(
790+
f"✓ Detected version format requires target_prefix: {config.target_prefix}"
791+
)
784792
else:
785793
# Interactive mode
786794
config = ask_library_questions()

cli/questions.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ def ask_library_questions() -> LibraryConfig:
3737
]
3838
rust_version_answer = inquirer.prompt(rust_version_question)
3939

40-
return LibraryConfig(
40+
config = LibraryConfig(
4141
language=language, name=rust_name_answer["name"], version=rust_version_answer["version"]
4242
)
43+
# Rust versions don't need git tag checking since they use crates.io
44+
return config
4345

4446
# For non-Rust languages, ask for GitHub URL
4547
github_question = [
@@ -97,7 +99,9 @@ def ask_library_questions() -> LibraryConfig:
9799
from pathlib import Path
98100

99101
cpp_handler = CppHandler(Path.home(), setup_ce_install=False, debug=False)
100-
is_valid, detected_type = cpp_handler.detect_library_type(github_answer["github_url"])
102+
is_valid, detected_type = cpp_handler.detect_library_type(
103+
github_answer["github_url"], library_id_answer["library_id"]
104+
)
101105

102106
if not is_valid:
103107
print("⚠️ Could not automatically detect library type.")
@@ -184,4 +188,14 @@ def ask_library_questions() -> LibraryConfig:
184188
is_c_library_answer = inquirer.prompt(is_c_library_question)
185189
config_data["is_c_library"] = is_c_library_answer["is_c_library"]
186190

187-
return LibraryConfig(**config_data)
191+
# Create the config and normalize versions with git lookup
192+
config = LibraryConfig(**config_data)
193+
194+
# Normalize versions by checking git tags (only for non-Rust)
195+
if language != Language.RUST:
196+
print("\nChecking git tags for version format...")
197+
config.normalize_versions_with_git_lookup()
198+
if config.target_prefix:
199+
print(f"✓ Detected version format requires target_prefix: {config.target_prefix}")
200+
201+
return config

core/c_handler.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .library_utils import (
1212
suggest_library_id_from_github_url,
1313
)
14-
from .models import LibraryConfig, LibraryType
14+
from .models import LibraryConfig, LibraryType, check_existing_library_config
1515
from .subprocess_utils import run_ce_install_command, run_command
1616

1717
logger = logging.getLogger(__name__)
@@ -37,13 +37,40 @@ def setup_ce_install(self) -> bool:
3737
"""Ensure ce_install is available"""
3838
return setup_ce_install_shared(self.infra_path, self.debug)
3939

40-
def detect_library_type(self, github_url: str) -> tuple[bool, LibraryType | None]:
40+
def detect_library_type(
41+
self, github_url: str, library_id: str | None = None
42+
) -> tuple[bool, LibraryType | None]:
4143
"""
4244
Clone repository and detect if it's header-only by checking for CMakeLists.txt.
45+
Also checks existing library configuration if available.
4346
4447
Returns:
4548
Tuple of (is_valid, library_type)
4649
"""
50+
# First check if library already exists and use its configuration
51+
if (
52+
library_id
53+
and hasattr(self, "infra_path")
54+
and self.infra_path
55+
and self.infra_path.exists()
56+
):
57+
existing_config = check_existing_library_config(github_url, library_id, self.infra_path)
58+
if existing_config:
59+
# Library exists, try to determine type from existing config
60+
if existing_config.get("type") == "header-only":
61+
logger.info(f"Using existing configuration: {library_id} is header-only")
62+
return True, LibraryType.HEADER_ONLY
63+
elif existing_config.get("type") == "packaged-headers":
64+
logger.info(f"Using existing configuration: {library_id} is packaged-headers")
65+
return True, LibraryType.PACKAGED_HEADERS
66+
elif existing_config.get("type") == "static":
67+
logger.info(f"Using existing configuration: {library_id} is static")
68+
return True, LibraryType.STATIC
69+
elif existing_config.get("type") == "shared":
70+
logger.info(f"Using existing configuration: {library_id} is shared")
71+
return True, LibraryType.SHARED
72+
# If existing config doesn't have clear type info, continue with detection
73+
4774
with tempfile.TemporaryDirectory() as tmpdir:
4875
clone_path = Path(tmpdir) / "repo"
4976

core/cpp_handler.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
from .library_utils import (
1313
suggest_library_id_from_github_url,
1414
)
15-
from .models import LibraryConfig, LibraryType
15+
from .models import (
16+
LibraryConfig,
17+
LibraryType,
18+
check_existing_library_config,
19+
check_existing_library_config_remote,
20+
)
1621
from .subprocess_utils import run_ce_install_command, run_command
1722

1823
logger = logging.getLogger(__name__)
@@ -38,13 +43,72 @@ def setup_ce_install(self) -> bool:
3843
"""Ensure ce_install is available"""
3944
return setup_ce_install_shared(self.infra_path, self.debug)
4045

41-
def detect_library_type(self, github_url: str) -> tuple[bool, LibraryType | None]:
46+
def detect_library_type(
47+
self, github_url: str, library_id: str | None = None
48+
) -> tuple[bool, LibraryType | None]:
4249
"""
4350
Clone repository and detect if it's header-only by checking for CMakeLists.txt.
51+
Also checks existing library configuration if available.
4452
4553
Returns:
4654
Tuple of (is_valid, library_type)
4755
"""
56+
# First check if library already exists and use its configuration
57+
existing_config = None
58+
if (
59+
library_id
60+
and hasattr(self, "infra_path")
61+
and self.infra_path
62+
and self.infra_path.exists()
63+
and (self.infra_path / "bin" / "yaml" / "libraries.yaml").exists()
64+
):
65+
# We have the infra repo locally with libraries.yaml
66+
existing_config = check_existing_library_config(github_url, library_id, self.infra_path)
67+
elif library_id:
68+
# We don't have the repo yet (interactive mode), check remotely
69+
logger.info(f"Checking for existing configuration of {library_id}...")
70+
existing_config = check_existing_library_config_remote(github_url, library_id)
71+
72+
if existing_config:
73+
# Library exists, try to determine type from existing config
74+
75+
# Check if it's explicitly marked as header-only via build_type
76+
build_type = existing_config.get("build_type")
77+
if build_type == "none":
78+
logger.info(
79+
f"Using existing configuration: {library_id} is header-only (build_type: none)"
80+
)
81+
return True, LibraryType.HEADER_ONLY
82+
83+
# Check legacy type field
84+
lib_type = existing_config.get("type")
85+
if lib_type == "header-only":
86+
logger.info(f"Using existing configuration: {library_id} is header-only")
87+
return True, LibraryType.HEADER_ONLY
88+
elif lib_type == "packaged-headers":
89+
logger.info(f"Using existing configuration: {library_id} is packaged-headers")
90+
return True, LibraryType.PACKAGED_HEADERS
91+
elif lib_type == "static":
92+
logger.info(f"Using existing configuration: {library_id} is static")
93+
return True, LibraryType.STATIC
94+
elif lib_type == "shared":
95+
logger.info(f"Using existing configuration: {library_id} is shared")
96+
return True, LibraryType.SHARED
97+
98+
# If it's type: github with no explicit build_type, it might be header-only by default
99+
if lib_type == "github" and build_type is None:
100+
logger.info(
101+
f"Using existing configuration: {library_id} is likely header-only "
102+
f"(type: github, no build_type)"
103+
)
104+
return True, LibraryType.HEADER_ONLY
105+
106+
# If existing config doesn't have clear type info, continue with detection
107+
logger.warning(
108+
f"Could not determine type from existing config for {library_id}, "
109+
f"falling back to detection"
110+
)
111+
48112
with tempfile.TemporaryDirectory() as tmpdir:
49113
clone_path = Path(tmpdir) / "repo"
50114

core/models.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from __future__ import annotations
22

3+
import subprocess
34
from enum import Enum
5+
from pathlib import Path
46

7+
import yaml
58
from pydantic import BaseModel, HttpUrl, field_validator
69

710

@@ -29,10 +32,158 @@ class LibraryType(str, Enum):
2932
SHARED = "shared"
3033

3134

35+
def check_git_tag_exists(repo_url: str, tag: str) -> bool:
36+
"""Check if a git tag exists in the remote repository"""
37+
try:
38+
# Check if tag exists remotely without cloning
39+
result = subprocess.run(
40+
["git", "ls-remote", "--tags", repo_url, f"refs/tags/{tag}"],
41+
capture_output=True,
42+
text=True,
43+
timeout=30,
44+
)
45+
return result.returncode == 0 and result.stdout.strip() != ""
46+
except (subprocess.TimeoutExpired, subprocess.SubprocessError):
47+
return False
48+
49+
50+
def check_existing_library_config(
51+
repo_url: str, library_id: str, infra_repo_path: Path | None = None
52+
) -> dict | None:
53+
"""
54+
Check if a library already exists in libraries.yaml and return its configuration.
55+
Returns None if not found, or a dict with the library configuration if found.
56+
"""
57+
if not infra_repo_path:
58+
return None
59+
60+
libraries_yaml_path = infra_repo_path / "bin" / "yaml" / "libraries.yaml"
61+
62+
if not libraries_yaml_path.exists():
63+
return None
64+
65+
try:
66+
with open(libraries_yaml_path, encoding="utf-8") as f:
67+
libraries_data = yaml.safe_load(f)
68+
69+
if not isinstance(libraries_data, dict):
70+
return None
71+
72+
# The structure is: libraries -> language -> library_name
73+
libraries_section = libraries_data.get("libraries", {})
74+
if not libraries_section:
75+
return None
76+
77+
# Check in different language sections (c, cpp, rust, etc.)
78+
for lang_libs in libraries_section.values():
79+
if not isinstance(lang_libs, dict):
80+
continue
81+
82+
# Check if library_id exists in this language section
83+
if library_id in lang_libs:
84+
return lang_libs[library_id]
85+
86+
# Also check by GitHub URL/repo since library_id might be different
87+
for lib_config in lang_libs.values():
88+
if isinstance(lib_config, dict):
89+
lib_url = lib_config.get("url")
90+
lib_repo = lib_config.get("repo")
91+
92+
if lib_url == repo_url:
93+
return lib_config
94+
95+
# Also check repo field which might be in format "owner/repo"
96+
if lib_repo and repo_url.endswith(f"/{lib_repo}"):
97+
return lib_config
98+
99+
return None
100+
101+
except (yaml.YAMLError, FileNotFoundError, PermissionError):
102+
return None
103+
104+
105+
def check_existing_library_config_remote(repo_url: str, library_id: str) -> dict | None:
106+
"""
107+
Check if a library already exists by temporarily cloning the infra repository.
108+
This is used during interactive detection when we don't have the repo yet.
109+
"""
110+
import tempfile
111+
112+
from .subprocess_utils import run_command
113+
114+
try:
115+
with tempfile.TemporaryDirectory() as tmpdir:
116+
infra_path = Path(tmpdir) / "infra"
117+
118+
# Clone the infra repository
119+
result = run_command(
120+
[
121+
"git",
122+
"clone",
123+
"--depth",
124+
"1",
125+
"https://github.com/compiler-explorer/infra",
126+
str(infra_path),
127+
],
128+
clean_env=False,
129+
)
130+
131+
if result.returncode != 0:
132+
return None
133+
134+
return check_existing_library_config(repo_url, library_id, infra_path)
135+
136+
except Exception:
137+
return None
138+
139+
140+
def determine_version_format(repo_url: str, version: str) -> tuple[str, str | None]:
141+
"""
142+
Determine the actual version format by checking git tags.
143+
Returns (normalized_version, target_prefix)
144+
"""
145+
if not repo_url:
146+
# No repo URL, can't check tags - just normalize by removing 'v' prefix
147+
if version.startswith("v"):
148+
return version[1:], "v"
149+
return version, None
150+
151+
# Check if version starts with 'v'
152+
if version.startswith("v"):
153+
# User entered v1.2.3 - check if both v1.2.3 and 1.2.3 exist
154+
version_with_v = version
155+
version_without_v = version[1:]
156+
157+
if check_git_tag_exists(repo_url, version_with_v):
158+
# v1.2.3 exists - use it and set target_prefix
159+
return version_without_v, "v"
160+
elif check_git_tag_exists(repo_url, version_without_v):
161+
# Only 1.2.3 exists - user made a mistake, use 1.2.3
162+
return version_without_v, None
163+
else:
164+
# Neither exists - assume user intended v1.2.3 format
165+
return version_without_v, "v"
166+
else:
167+
# User entered 1.2.3 - check if both 1.2.3 and v1.2.3 exist
168+
version_without_v = version
169+
version_with_v = f"v{version}"
170+
171+
if check_git_tag_exists(repo_url, version_without_v):
172+
# 1.2.3 exists - use it without prefix
173+
return version_without_v, None
174+
elif check_git_tag_exists(repo_url, version_with_v):
175+
# Only v1.2.3 exists - should use target_prefix
176+
return version_without_v, "v"
177+
else:
178+
# Neither exists - assume user intended 1.2.3 format
179+
return version_without_v, None
180+
181+
32182
class LibraryConfig(BaseModel):
33183
language: Language
34184
github_url: HttpUrl | None = None
35185
version: str | list[str] # Support single version or list of versions
186+
target_prefix: str | None = None # Prefix for version tags (e.g., 'v')
36187
is_header_only: bool | None = None
37188
build_tool: BuildTool | None = None
38189
link_type: LinkType | None = None
@@ -64,6 +215,35 @@ def validate_version(cls, v):
64215
else:
65216
raise ValueError("Version must be a string or list of strings")
66217

218+
def normalize_versions_with_git_lookup(self):
219+
"""
220+
Normalize versions by checking git tags and set target_prefix if needed.
221+
This should be called after the model is fully populated with github_url.
222+
"""
223+
if not self.github_url:
224+
return
225+
226+
versions = self.get_versions()
227+
normalized_versions = []
228+
target_prefix = None
229+
230+
for version in versions:
231+
normalized_version, prefix = determine_version_format(str(self.github_url), version)
232+
normalized_versions.append(normalized_version)
233+
234+
# Set target_prefix if any version needs it
235+
if prefix:
236+
target_prefix = prefix
237+
238+
# Update the model
239+
if len(normalized_versions) == 1:
240+
self.version = normalized_versions[0]
241+
else:
242+
self.version = normalized_versions
243+
244+
if target_prefix:
245+
self.target_prefix = target_prefix
246+
67247
def get_versions(self) -> list[str]:
68248
"""Get list of versions, handling both single and multiple version cases"""
69249
if isinstance(self.version, str):

0 commit comments

Comments
 (0)