|
47 | 47 | //! rewriter could not have written in place (a blank line before it — |
48 | 48 | //! the in-place match swallows every blank line before the declaration, |
49 | 49 | //! and the append always writes one; it goes with the block) that |
50 | | -//! another locked spec depends on. Otherwise the gem is kept as a |
| 50 | +//! another locked spec depends on (a spec sub-line, or a Bundler 4.1 |
| 51 | +//! `OVERRIDES` `with:` target, #1989). Otherwise the gem is kept as a |
51 | 52 | //! direct pin: a stray exact pin installs the same bytes, while dropping |
52 | 53 | //! a real declaration would stop `Bundler.require` loading the gem. |
53 | 54 | //! * a `CHECKSUMS` entry the rewriter ADDED is only recognizable next to |
@@ -173,11 +174,34 @@ fn dependency_line<'l>(lines: &[&'l str], name: &str) -> Option<(usize, &'l str) |
173 | 174 | }) |
174 | 175 | } |
175 | 176 |
|
176 | | -/// Whether another locked spec depends on `name` (a 6-space sub-line). |
| 177 | +/// Whether another locked spec depends on `name`: a 6-space sub-line, or |
| 178 | +/// the `with:` target of a Bundler 4.1 `OVERRIDES` entry (#1989). An |
| 179 | +/// `override "other", with: "name", in: "parent"` keeps `parent`'s |
| 180 | +/// original edge to `other` in the specs, so no sub-line names `name`. |
177 | 181 | fn is_subdependency(lines: &[&str], name: &str) -> bool { |
178 | 182 | lines |
179 | 183 | .iter() |
180 | 184 | .any(|l| indented(l, 6).is_some_and(|d| d.split([' ', '(']).next() == Some(name))) |
| 185 | + || override_targets(lines).any(|t| t == name) |
| 186 | +} |
| 187 | + |
| 188 | +/// The `with:` values of the lock's `OVERRIDES` entries |
| 189 | +/// (` "other", with: "name", in: "parent"`). |
| 190 | +fn override_targets<'l>(lines: &'l [&'l str]) -> impl Iterator<Item = &'l str> + 'l { |
| 191 | + let range = named_section(lines, "OVERRIDES").unwrap_or((0, 0)); |
| 192 | + lines[range.0..range.1].iter().filter_map(|l| { |
| 193 | + let entry = indented(l, 2)?.trim_end(); |
| 194 | + // Bundler writes `"<target>", with: "<gem>", in: "<parent>"` |
| 195 | + // (`Override#to_lock`: `inspect`ed values joined by `, `). A |
| 196 | + // `version:` value may itself hold `, `, so splitting on it can |
| 197 | + // break that value apart, but never the `with:` one: a gem name |
| 198 | + // holds no `"`, `,` or space. |
| 199 | + entry.split(", ").find_map(|arg| { |
| 200 | + arg.strip_prefix("with: \"")? |
| 201 | + .strip_suffix('"') |
| 202 | + .filter(|v| !v.contains('"')) |
| 203 | + }) |
| 204 | + }) |
181 | 205 | } |
182 | 206 |
|
183 | 207 | /// How the lock wires patch `uuid`. |
@@ -1870,6 +1894,46 @@ mod tests { |
1870 | 1894 | assert!(!is_subdependency(&[" rails (7.0.0)"], "rails")); |
1871 | 1895 | } |
1872 | 1896 |
|
| 1897 | + /// #1989: Bundler 4.1's `override "other", with: "rails", in: "puma"` |
| 1898 | + /// keeps `puma`'s original edge to `other` in the specs and records |
| 1899 | + /// the replacement only in `OVERRIDES`, so its `with:` target is a |
| 1900 | + /// dependency of another spec although no sub-line names it. |
| 1901 | + #[test] |
| 1902 | + fn an_overrides_with_target_is_a_subdependency() { |
| 1903 | + let lock = |overrides: &str| { |
| 1904 | + format!( |
| 1905 | + "GEM\n remote: https://rubygems.org/\n specs:\n puma (6.0.0)\n \ |
| 1906 | + other (>= 1)\n rails (7.0.0)\n\nDEPENDENCIES\n puma\n\nOVERRIDES\n\ |
| 1907 | + {overrides}\nBUNDLED WITH\n 4.1.0.beta3\n" |
| 1908 | + ) |
| 1909 | + }; |
| 1910 | + let has = |overrides: &str| { |
| 1911 | + let text = lock(overrides); |
| 1912 | + let lines: Vec<&str> = text.split('\n').collect(); |
| 1913 | + is_subdependency(&lines, "rails") |
| 1914 | + }; |
| 1915 | + assert!(has(" \"other\", with: \"rails\", in: \"puma\"\n")); |
| 1916 | + assert!(has(" \"other\", in: \"puma\", with: \"rails\"\n")); |
| 1917 | + // A replacement version requirement, as Bundler 4.1 locks it. |
| 1918 | + assert!(has( |
| 1919 | + " \"other\", with: \"rails\", in: \"puma\", version: \">= 7, < 8\"\n" |
| 1920 | + )); |
| 1921 | + assert!(has( |
| 1922 | + " \"x\", with: \"y\", in: \"z\"\n \"other\", with: \"rails\", in: \"puma\"\n" |
| 1923 | + )); |
| 1924 | + // `rails` as the overridden name, the parent, or a prefix is not |
| 1925 | + // a `with:` target. |
| 1926 | + assert!(!has(" \"rails\", with: \"other\", in: \"puma\"\n")); |
| 1927 | + assert!(!has(" \"other\", with: \"x\", in: \"rails\"\n")); |
| 1928 | + assert!(!has(" \"other\", with: \"rails-html\", in: \"puma\"\n")); |
| 1929 | + // `with: nil` drops the dependency: no target. |
| 1930 | + assert!(!has(" \"rails\", with: nil, in: \"puma\"\n")); |
| 1931 | + assert!(!has("")); |
| 1932 | + // A `with:` entry outside the OVERRIDES section is not read. |
| 1933 | + let lines = ["DEPENDENCIES", " \"other\", with: \"rails\", in: \"puma\""]; |
| 1934 | + assert!(!is_subdependency(&lines, "rails")); |
| 1935 | + } |
| 1936 | + |
1873 | 1937 | /// #457: what the hosted rewriter itself writes for a TRANSITIVE gem |
1874 | 1938 | /// (an appended block, converged CHECKSUMS lock) must be recognized as |
1875 | 1939 | /// its append, and undoing it must give the manifest back byte for |
@@ -2062,6 +2126,71 @@ mod tests { |
2062 | 2126 | assert_eq!(next_lock.as_deref(), Some(lock.as_str())); |
2063 | 2127 | } |
2064 | 2128 |
|
| 2129 | + /// #1989: a gem reached only through a Bundler 4.1 `override …, with:` |
| 2130 | + /// is transitive too. The lock keeps the parent's original edge, so no |
| 2131 | + /// spec sub-line names it; the restore must still drop the appended |
| 2132 | + /// block and its DEPENDENCIES entry, not write a new exact pin. |
| 2133 | + #[tokio::test] |
| 2134 | + async fn override_target_redirect_round_trips_through_restore() { |
| 2135 | + let client = super::super::UpstreamClient::new(true); |
| 2136 | + let upstream_sha = "2".repeat(64); |
| 2137 | + client |
| 2138 | + .seed_rubygems_sha256("rails", "7.0.0", &upstream_sha) |
| 2139 | + .await; |
| 2140 | + let ctx = ctx_with(&client); |
| 2141 | + let manifest = "source \"https://rubygems.org\"\n\ngem \"puma\"\n\n\ |
| 2142 | + override \"other\", with: \"rails\", in: \"puma\"\n"; |
| 2143 | + let lock = format!( |
| 2144 | + "GEM\n remote: https://rubygems.org/\n specs:\n puma (6.0.0)\n other\n \ |
| 2145 | + rails (7.0.0)\n\nPLATFORMS\n ruby\n\nDEPENDENCIES\n puma\n\nOVERRIDES\n \ |
| 2146 | + \"other\", with: \"rails\", in: \"puma\"\n\nCHECKSUMS\n puma (6.0.0) sha256={}\n \ |
| 2147 | + rails (7.0.0) sha256={upstream_sha}\n\nBUNDLED WITH\n 4.1.0.beta3\n", |
| 2148 | + "a".repeat(64) |
| 2149 | + ); |
| 2150 | + let files = BTreeMap::from([ |
| 2151 | + ("Gemfile".to_string(), manifest.to_string()), |
| 2152 | + ("Gemfile.lock".to_string(), lock.clone()), |
| 2153 | + ]); |
| 2154 | + let r = crate::patch::redirect::rewrite_registry_redirect( |
| 2155 | + &files, |
| 2156 | + &[crate::patch::redirect::DepOverride { |
| 2157 | + ecosystem: "gem".into(), |
| 2158 | + name: "rails".into(), |
| 2159 | + namespace: None, |
| 2160 | + version: "7.0.0".into(), |
| 2161 | + token: "tok".into(), |
| 2162 | + patch_uuid: UUID.into(), |
| 2163 | + artifact_url: "https://patch.test/rails-7.0.0.gem".into(), |
| 2164 | + registry_override: Some(crate::patch::redirect::RegistryOverride { |
| 2165 | + kind: "rubygems-compact-index".into(), |
| 2166 | + index_url: IDX.into(), |
| 2167 | + identifiers: crate::patch::redirect::RegistryOverrideIdentifiers { |
| 2168 | + name: "rails".into(), |
| 2169 | + version: "7.0.0".into(), |
| 2170 | + gem_checksum_sha256: Some("f".repeat(64)), |
| 2171 | + ..Default::default() |
| 2172 | + }, |
| 2173 | + }), |
| 2174 | + integrity: Default::default(), |
| 2175 | + }], |
| 2176 | + ); |
| 2177 | + let (hosted_manifest, hosted_lock) = (&r.files["Gemfile"], &r.files["Gemfile.lock"]); |
| 2178 | + assert!(hosted_lock.contains(" rails (= 7.0.0)!"), "{hosted_lock}"); |
| 2179 | + let (next_lock, next_manifest, _) = restore_one( |
| 2180 | + &gem(), |
| 2181 | + Some(hosted_lock), |
| 2182 | + Some(hosted_manifest), |
| 2183 | + "Gemfile", |
| 2184 | + &global_sources(hosted_manifest), |
| 2185 | + &ctx, |
| 2186 | + ) |
| 2187 | + .await |
| 2188 | + .unwrap() |
| 2189 | + .expect("the redirect is found"); |
| 2190 | + assert_eq!(next_manifest.as_deref(), Some(manifest)); |
| 2191 | + assert_eq!(next_lock.as_deref(), Some(lock.as_str())); |
| 2192 | + } |
| 2193 | + |
2065 | 2194 | /// #1056: a gem the user declared inside a `source "…" do` block is |
2066 | 2195 | /// locked with a `!` source pin in `DEPENDENCIES` (bundler writes it |
2067 | 2196 | /// for any dependency with an explicit source, rubygems.org included). |
|