Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 9 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion crates/lpm-cli/src/commands/add/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,11 @@ async fn run_locked(
&& metadata.resolve_version_spec(requested).is_err()
{
metadata = match &target {
AddTarget::Lpm(package) => client.refetch_package_metadata(package).await?,
AddTarget::Lpm(package) => {
client
.refetch_package_metadata_after_missing_version(package)
.await?
}
AddTarget::Npm { spec } => {
let route = route_table.route_for_package(spec);
match &route {
Expand Down
4 changes: 3 additions & 1 deletion crates/lpm-cli/src/commands/install/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,9 @@ pub(super) async fn resolve_lpm_install_preflight(
| LpmError::InvalidVersionRange(_)
| LpmError::PublicationUnavailable(_),
) => {
let metadata = client.refetch_package_metadata(package_name).await?;
let metadata = client
.refetch_package_metadata_after_missing_version(package_name)
.await?;
let version = resolve_lpm_install_metadata_version(&metadata, package_name, range)?;
Ok((metadata, version))
}
Expand Down
37 changes: 34 additions & 3 deletions crates/lpm-cli/src/commands/install/swift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,19 @@ pub(super) async fn run_swift_install_xcode_batch(
let project_root = xcodeproj_path.parent().unwrap_or(project_dir);
let wrapper = crate::swift_manifest::ensure_wrapper_package(project_root)?;
let wrapper_dir = wrapper.manifest_path.parent().unwrap_or(project_root);
let mut setup = crate::commands::swift_registry::ensure_configured(
let coordinates = requests
.iter()
.map(|request| {
lpm_registry::ManagedInstallRoot::new(request.name.scoped(), request.version)
})
.collect::<Vec<_>>();
let anonymous = options.client.can_install_anonymously(&coordinates).await?;
let mut setup = crate::commands::swift_registry::ensure_configured_for_install(
options.client.session().map(|session| session.as_ref()),
options.client.base_url(),
wrapper_dir,
options.json_output,
anonymous,
)
.await?;
setup.include_scope_repair(
Expand Down Expand Up @@ -241,6 +249,7 @@ async fn finish_swift_batch(
)
})
.collect::<Vec<_>>();
let anonymous = options.client.can_install_anonymously(&coordinates).await?;
let warnings = options.client.check_install_access(&coordinates).await?;
if !options.json_output {
for warning in warnings {
Expand All @@ -252,19 +261,41 @@ async fn finish_swift_batch(
None => (
None,
&[][..],
crate::commands::swift_registry::ensure_configured(
crate::commands::swift_registry::ensure_configured_for_install(
options.client.session().map(|session| session.as_ref()),
options.client.base_url(),
resolve_dir,
options.json_output,
anonymous,
)
.await?,
),
};
if !options.json_output {
output::info("Resolving Swift packages...");
}
crate::swift_manifest::run_swift_resolve_with_force(resolve_dir, options.force)?;
if let Err(error) =
crate::swift_manifest::run_swift_resolve_with_force(resolve_dir, options.force)
{
let has_credential = options
.client
.session()
.map(|session| session.current_source())
.transpose()?
.flatten()
.is_some();
if !anonymous || !has_credential {
return Err(error);
}
crate::commands::swift_registry::ensure_configured(
options.client.session().map(|session| session.as_ref()),
options.client.base_url(),
resolve_dir,
options.json_output,
)
.await?;
crate::swift_manifest::run_swift_resolve_with_force(resolve_dir, options.force)?;
}
crate::xcode_project::native::resolve(containers)?;
let resolved = crate::swift_manifest::validate_swift_dependency_graph(resolve_dir)?;
let coordinates = resolved
Expand Down
36 changes: 35 additions & 1 deletion crates/lpm-cli/src/commands/swift_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,16 @@ pub async fn ensure_configured(
registry_url: &str,
package_dir: &std::path::Path,
json_output: bool,
) -> Result<SwiftRegistrySetupOutcome, LpmError> {
ensure_configured_for_install(session, registry_url, package_dir, json_output, false).await
}

pub(crate) async fn ensure_configured_for_install(
session: Option<&lpm_auth::SessionManager>,
registry_url: &str,
package_dir: &std::path::Path,
json_output: bool,
anonymous: bool,
) -> Result<SwiftRegistrySetupOutcome, LpmError> {
if !lpm_common::lpm_registry_url_is_accepted(registry_url) {
return Err(LpmError::Registry(format!(
Expand Down Expand Up @@ -486,7 +496,7 @@ pub async fn ensure_configured(
}
}

if is_https {
if is_https && !anonymous {
let discovered_session;
let session = match session {
Some(session) => session,
Expand Down Expand Up @@ -1231,6 +1241,30 @@ exit 64
);
}

#[cfg(unix)]
#[tokio::test]
async fn anonymous_https_setup_skips_rejected_credentials_but_requires_certificate_verification()
{
let _lock = home_env_lock().lock().await;
let home = TempDir::new().unwrap();
let package = TempDir::new().unwrap();
let path = fake_swift_path(home.path());
let _environment = crate::test_env::ScopedEnv::update([
("HOME", Some(home.path().as_os_str().to_owned())),
("PATH", Some(path)),
("LPM_TOKEN", None),
]);
let registry_url = "https://127.0.0.1:1";
write_matching_package_scope(package.path(), registry_url);
let session = lpm_auth::SessionManager::new(registry_url, Some("rejected-access".into()));
let error =
ensure_configured_for_install(Some(&session), registry_url, package.path(), true, true)
.await
.expect_err("unreachable certificate endpoint must prevent setup");
assert!(!home.path().join("swift-login-tokens").exists());
assert!(error.to_string().contains("certificate"), "{error}");
}

#[cfg(unix)]
#[tokio::test]
async fn automatic_setup_reports_swiftpm_login_failure() {
Expand Down
121 changes: 87 additions & 34 deletions crates/lpm-cli/src/swift_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -946,35 +946,38 @@ fn insert_into_dependencies_array(
let (package_open, package_close) = find_package_call(content)
.ok_or_else(|| LpmError::Registry("Could not find Package(...) in Package.swift".into()))?;

let (bracket_start, close_pos) =
match find_direct_argument_array(content, package_open, package_close, "dependencies") {
Some(array) => (array.open, array.close),
None => {
if let Some(kw_pos) =
find_direct_argument_label(content, package_open, package_close, "targets")
{
let kw_indent = get_line_indent(content, kw_pos);
let entry_indent = indent_one_level(&kw_indent);
let new_deps = format!(
"{}dependencies: [\n{}{},\n{}],\n",
kw_indent, entry_indent, entry, kw_indent
);
// Insert the new dependencies array on a new line before the keyword line.
// content[line_start..] already includes the keyword's own indentation,
// so we don't append kw_indent again.
let line_start = content[..kw_pos].rfind('\n').map_or(kw_pos, |i| i + 1);
let mut new_content =
String::with_capacity(content.len() + new_deps.len() + 10);
new_content.push_str(&content[..line_start]);
new_content.push_str(&new_deps);
new_content.push_str(&content[line_start..]);
return Ok(new_content);
}
return Err(LpmError::Registry(
"Could not find 'dependencies:' in Package.swift".into(),
));
let (bracket_start, close_pos) = match find_direct_argument_array(
content,
package_open,
package_close,
"dependencies",
) {
Some(array) => (array.open, array.close),
None => {
if let Some(kw_pos) =
find_direct_argument_label(content, package_open, package_close, "targets")
{
let kw_indent = get_line_indent(content, kw_pos);
let entry_indent = indent_one_level(&kw_indent);
let line_start = content[..kw_pos].rfind('\n').map_or(0, |i| i + 1);
let on_own_line = content[line_start..kw_pos].trim().is_empty();
let insertion = if on_own_line { line_start } else { kw_pos };
let leading_indent = if on_own_line { kw_indent.as_str() } else { "" };
let trailing_indent = if on_own_line { "" } else { kw_indent.as_str() };
let new_deps = format!(
"{leading_indent}dependencies: [\n{entry_indent}{entry},\n{kw_indent}],\n{trailing_indent}"
);
let mut new_content = String::with_capacity(content.len() + new_deps.len());
new_content.push_str(&content[..insertion]);
new_content.push_str(&new_deps);
new_content.push_str(&content[insertion..]);
return Ok(new_content);
}
};
return Err(LpmError::Registry(
"Could not find 'dependencies:' in Package.swift".into(),
));
}
};

// Detect indentation from existing entries or derive from context
let indent = detect_indent(content, bracket_start, close_pos);
Expand Down Expand Up @@ -1002,9 +1005,13 @@ fn insert_into_dependencies_array(
let needs_comma = !before_close.ends_with(',');

// Find the start of the line containing `]` — we'll insert before it
let close_line_start = content[..close_pos]
.rfind('\n')
.map_or(close_pos, |i| i + 1);
let close_line_start = content[..close_pos].rfind('\n').map_or(0, |i| i + 1);
let close_on_own_line = content[close_line_start..close_pos].trim().is_empty();
let close_line_start = if close_on_own_line {
close_line_start
} else {
close_pos
};

let mut new_content = String::with_capacity(content.len() + entry.len() + 20);

Expand All @@ -1019,6 +1026,9 @@ fn insert_into_dependencies_array(
} else {
// Content up to the close line (everything before the `]` line)
new_content.push_str(&content[..close_line_start]);
if !close_on_own_line {
new_content.push('\n');
}
}

new_content.push_str(&indent);
Expand Down Expand Up @@ -1094,9 +1104,13 @@ fn insert_into_target_deps(
let before_close = content[bracket_start + 1..close_pos].trim_end();
let needs_comma = !before_close.ends_with(',');

let close_line_start = content[..close_pos]
.rfind('\n')
.map_or(close_pos, |i| i + 1);
let close_line_start = content[..close_pos].rfind('\n').map_or(0, |i| i + 1);
let close_on_own_line = content[close_line_start..close_pos].trim().is_empty();
let close_line_start = if close_on_own_line {
close_line_start
} else {
close_pos
};

let mut new_content = String::with_capacity(content.len() + entry.len() + 20);

Expand All @@ -1109,6 +1123,9 @@ fn insert_into_target_deps(
new_content.push('\n');
} else {
new_content.push_str(&content[..close_line_start]);
if !close_on_own_line {
new_content.push('\n');
}
}

new_content.push_str(&indent);
Expand Down Expand Up @@ -2209,6 +2226,42 @@ let package = Package(
}

// === verify insert creates deps array in target ===
#[test]
fn registry_dependencies_preserve_inline_package_arguments() {
let entry = ".package(id: \"lpmdev.acme_logger\", from: \"1.0.0\")";
for dependencies in [
"",
"dependencies: [.package(url: \"https://example.com/a\", from: \"1.0.0\")], ",
"dependencies: [.package(url: \"https://example.com/a\", from: \"1.0.0\"),], ",
] {
let input = format!(
"import PackageDescription\nlet package = Package(name: \"App\", {dependencies}targets: [.target(name: \"App\")])\n"
);
let output = insert_into_dependencies_array(&input, entry, Some("targets:")).unwrap();
assert_eq!(
output.matches("let package = Package(").count(),
1,
"{output}"
);
let array = find_package_argument_array(&output, "dependencies")
.unwrap_or_else(|| panic!("Package dependencies are missing: {output}"));
let calls = direct_calls_in_array(&output, array);
assert_eq!(
calls.len(),
if dependencies.is_empty() { 1 } else { 2 },
"{output}"
);
assert!(
output[..array.open].contains("let package = Package(name: \"App\","),
"{output}"
);
assert!(
output.contains("targets: [.target(name: \"App\")]"),
"{output}"
);
}
}

#[test]
fn insert_into_target_deps_creates_dependencies_array_when_target_lacks_one() {
let input = r#"// swift-tools-version: 5.9
Expand Down
2 changes: 1 addition & 1 deletion crates/lpm-registry/src/client/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ impl RegistryClient {
if let Some(v) = version {
url.push_str(&format!("&version={}", urlencoding::encode(v)));
}
self.execute_with_recovery(AuthPosture::AuthRequired, || self.get_json(&url))
self.execute_with_recovery(AuthPosture::PackageRead, || self.get_json(&url))
.await
}

Expand Down
Loading