Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/anolisa/crates/anolisa-cli/src/repo_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,6 @@ pub fn raw_index_v2_url(base_url: &str) -> String {
///
/// Released clients continue reading this path, so publishers must preserve its
/// schema v1 contents when publishing later component-index generations.
#[cfg(test)]
pub fn component_index_url(base_url: &str) -> String {
format!("{}/components.toml", raw_root(base_url))
}
Expand Down
51 changes: 43 additions & 8 deletions src/anolisa/crates/anolisa-cli/src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use anolisa_platform::pkg_query::{PackageQuery, PackageQueryError};
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::repo_config::{BackendConfig, HostVars, RepoConfig, component_index_v2_url};
use crate::repo_config::{BackendConfig, HostVars, RepoConfig, component_index_url, component_index_v2_url};

/// On-disk schema version for repo-side `components-v2.toml`.
pub(crate) const COMPONENT_INDEX_SCHEMA_VERSION: u32 = 2;
Expand Down Expand Up @@ -54,6 +54,7 @@ pub(crate) struct ComponentIndexEntry {
#[serde(default)]
pub(crate) summary: Option<String>,
/// Supported host OS/architecture combinations.
#[serde(default)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Populate targets when accepting v1 indexes

When the fallback loads a schema v1 components.toml, existing v1 rows use platforms = [...] rather than targets, so defaulting targets to an empty list makes supports_target() false for every component. In particular, install --all filters through component_names_for_target() and will report nothing to install even though the fallback index was loaded; translate v1 platform data into target availability or avoid target filtering for schema v1.

Useful? React with 👍 / 👎.

pub(crate) targets: Vec<ComponentTarget>,
/// Backend-native package names for this component.
///
Expand Down Expand Up @@ -473,7 +474,11 @@ impl ComponentIndex {
}

fn validate(&self) -> Result<(), ComponentIndexError> {
if self.schema_version != COMPONENT_INDEX_SCHEMA_VERSION {
// Accept schema v1 and v2: v1 rows simply lack `targets`, which
// defaults to an empty vec via `#[serde(default)]`. This lets the
// v2 parser gracefully load v1 `components.toml` when the v2 file
// has not been published to the mirror yet.
if !matches!(self.schema_version, 1 | 2) {
return Err(ComponentIndexError::UnsupportedSchema {
actual: self.schema_version,
expected: COMPONENT_INDEX_SCHEMA_VERSION,
Expand All @@ -492,7 +497,9 @@ impl ComponentIndex {
reason: format!("duplicate component '{name}'"),
});
}
if entry.targets.is_empty() {
// v1 indexes predate the `targets` field; skip the non-empty
// check for schema v1 (targets defaults to empty via serde).
if self.schema_version >= 2 && entry.targets.is_empty() {
return Err(ComponentIndexError::Invalid {
reason: format!("component '{name}' must declare at least one target"),
});
Expand Down Expand Up @@ -741,18 +748,43 @@ pub(crate) fn load_component_index(
let url = component_index_v2_url(&base_url);

let cache = DownloadCache::new(layout.cache_dir.clone());
let downloaded = match fetch_index_url(&cache, &url) {
Ok(art) => art,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] v1 回退逻辑依赖错误文案匹配,易受服务端文案变动影响

当前 v2→v1 回退通过 reason.contains("http status 404") 识别 404,resolution.rs:752-755。一旦底层 download 层调整错误信息格式(如本地化、移除 http status 前缀),v1 回退会静默失效,重新暴露 404 硬失败。建议显式携带/匹配 HTTP 状态码(或定义结构化错误枚举),在 404 时触发回退,避免对 free-text 文案的脆弱依赖。


🤖 Generated by QoderFix in Qoder

Err(ComponentIndexError::Fetch { ref reason })
if reason.contains("http status 404") =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat file:// missing v2 as absent

For local file:// repositories, a missing components-v2.toml is reported by DownloadCache::fetch as an I/O NotFound, not as an http status 404, so this guard never falls back to components.toml. That leaves v1-only local or mirrored repos failing to load even though the new code is meant to handle an unavailable v2 index; use typed absence detection like the raw index path does for HTTP 404/410 and file NotFound before wrapping the error.

Useful? React with 👍 / 👎.

{
// v2 not published yet — fall back to v1 components.toml.
// The v2 parser accepts v1 rows because `targets` has
// `#[serde(default)]`, so legacy indexes without targets
// still load (components simply appear as "no target info").
let v1_url = component_index_url(&base_url);
fetch_index_url(&cache, &v1_url).map_err(|err| ComponentIndexError::Fetch {
reason: format!(
"failed to fetch component index (tried v2 then v1): {err}"
),
})?
}
Err(err) => return Err(err),
};
ComponentIndex::load(&downloaded.cached_path)
}

/// Fetch a component index TOML from `url` into `cache`.
fn fetch_index_url(
cache: &DownloadCache,
url: &str,
) -> Result<anolisa_core::download::DownloadedArtifact, ComponentIndexError> {
#[cfg(test)]
if !url.starts_with("file://") {
return Err(ComponentIndexError::Fetch {
reason: format!("test mode: refusing non-file URL {url}"),
});
}
let downloaded = cache
.fetch(&url, None)
cache
.fetch(url, None)
.map_err(|err| ComponentIndexError::Fetch {
reason: format!("failed to fetch {url}: {err}"),
})?;
ComponentIndex::load(&downloaded.cached_path)
})
}

/// Best-effort load of repo-side `components-v2.toml`.
Expand Down Expand Up @@ -1325,7 +1357,10 @@ cosh = "site-copilot"

#[test]
fn unsupported_schema_is_rejected() {
for actual in [1, 99] {
// Schema v1 and v2 are accepted; anything else is rejected.
// v1 is accepted because `targets` defaults to an empty vec and
// the non-empty-target check is skipped for schema v1.
for actual in [99] {
let source = format!("schema_version = {actual}");
let err = ComponentIndex::from_toml_str(&source, "components.toml")
.expect_err("unsupported schema");
Expand Down
Loading