feat(core)!: keep reading a manifest that declares a usage kind we do not know - #557
vytautas-astrauskas-sensmetry wants to merge 3 commits into
Conversation
Signed-off-by: Vytautas Astrauskas <vytautas.astrauskas@sensmetry.com>
Signed-off-by: Vytautas Astrauskas <vytautas.astrauskas@sensmetry.com>
… not know Signed-off-by: Vytautas Astrauskas <vytautas.astrauskas@sensmetry.com>
I don't want to rely on this; manifest should be validated on any command that reads it, and I consider it an oversight that we don't do it already. To achieve the goal of this, PR, I suggest:
|
| // The catch-all `Unknown` variant is what keeps `.project.json` readable | ||
| // across versions: a manifest declaring a usage kind only a newer sysand | ||
| // knows still parses, so every command that does not have to interpret that | ||
| // usage keeps working and rewrites it untouched. Interpreting one is refused | ||
| // in `validate`, so an unknown dependency can never be silently dropped. | ||
| // | ||
| // It must stay the last variant: `untagged` tries variants in order and this | ||
| // one matches any JSON object. |
There was a problem hiding this comment.
Move this to Unknown variant to not repeat the same.
| InterchangeProjectUsageRaw::Unknown(unknown) => { | ||
| log::info!("{header}{removed:>12}{header:#} {unknown}"); | ||
| } |
There was a problem hiding this comment.
This is unreachable!() by definition; we don't know how to interpret this usage, so for sure can't remove it.
| // Never a standard library, which is all `excluded_iris` | ||
| // holds, and worth seeing: it is the one usage `info` can say | ||
| // nothing else about. |
There was a problem hiding this comment.
No need for this comment.
| // `validate` refuses a usage this build cannot interpret, so one | ||
| // never reaches the solver. Selecting nothing is the safe answer | ||
| // if that ever changes: it rules this candidate out rather than | ||
| // resolving a dependency sysand does not understand. | ||
| InterchangeProjectUsage::Unknown(_) => { | ||
| deps.push(( | ||
| DependencyIdentifier::Remote(usage.to_owned()), | ||
| DiscreteHashSet::empty(), | ||
| )); | ||
| } |
There was a problem hiding this comment.
This must panic instead of failing silently.
| // Unreachable in practice -- `validate` refuses an | ||
| // uninterpretable usage long before one is resolved -- but there | ||
| // is a truthful thing to print, so print it. | ||
| InterchangeProjectUsage::Unknown(unknown) => write!(f, "{unknown}")?, |
There was a problem hiding this comment.
It's an important invariant that this is unreachable!(), so use it.
| // `validate` refuses a usage this build cannot interpret, so one | ||
| // never reaches a resolver. Reporting it as unsupported rather | ||
| // than resolving it is the safe answer if that ever changes. | ||
| InterchangeProjectUsage::Unknown(unknown) => { | ||
| Ok(ResolutionOutcome::UnsupportedUsageType { | ||
| reason: unknown.to_string(), | ||
| }) | ||
| } |
There was a problem hiding this comment.
Unknown usage by definition will never be resolvable, so it must be marked unreachable!() in all places related to resolution.
| } | ||
| // `usage` passed `validate` above, which refuses an | ||
| // uninterpretable entry, so there is no merge path to take here. | ||
| InterchangeProjectUsageRaw::Unknown(_) => {} |
There was a problem hiding this comment.
unreachable!(); all such places must panic.
| } | ||
|
|
||
| #[test] | ||
| fn add_leaves_a_usage_it_cannot_interpret_alone() { |
There was a problem hiding this comment.
I would prefer to fail in this case, as no merging/duplicate detection can be performed, so the manifest may become invalid.
| // `validate` refuses a usage this build cannot interpret, so one | ||
| // never reaches a resolver. Reporting it as unsupported rather | ||
| // than resolving it is the safe answer if that ever changes. | ||
| InterchangeProjectUsage::Unknown(unknown) => { | ||
| Ok(ResolutionOutcome::UnsupportedUsageType { | ||
| reason: unknown.to_string(), | ||
| }) | ||
| } |
There was a problem hiding this comment.
unreachable!()
| .usage | ||
| .iter() | ||
| .map(|u| Identifier::from_interchange_usage_unchecked(u).into_string()) | ||
| .filter_map(|u| Identifier::from_unvalidated_usage(u).map(Identifier::into_string)) |
There was a problem hiding this comment.
Something already went wrong if this is reached, so this must panic on Unknown.
| .usage | ||
| .into_iter() | ||
| .map(|u| Identifier::from_interchange_usage_unchecked(&u).into_string()) | ||
| .filter_map(|u| Identifier::from_unvalidated_usage(&u).map(Identifier::into_string)) |
There was a problem hiding this comment.
Must panic on Unknown.
| | InterchangeProjectUsage::KparPath { .. } | ||
| | InterchangeProjectUsage::Unknown(_) => false, |
There was a problem hiding this comment.
unreachable!()
| None, | ||
| InterchangeProjectUsage::Directory { .. } | ||
| | InterchangeProjectUsage::KparPath { .. } | ||
| | InterchangeProjectUsage::Unknown(_) |
There was a problem hiding this comment.
unreachable!()
andrius-puksta-sensmetry
left a comment
There was a problem hiding this comment.
I guess supporting this cleanly would require introducing a new type, for which there is no time now. Current implementation is fine functionality-wise, but silently accepts incorrect behavior.
|
We decided to go in the opposite direction with #566. |
.project.jsondeserializes itsusagearray through an untagged enum withno catch-all, so an entry matching no known kind fails the whole document.
The day a fourth usage kind ships, every sysand already installed stops being
able to read those projects at all — not only
lockandsync, butinfo,addandremovetoo, and with serde's own wording:A version number does not reach a file format (see the PR below this one), so
that behaviour is not something a major bump can fix later. It has to be
fixed before the release, not after.
What changed
InterchangeProjectUsageG::Unknown(UnknownUsage), which keeps theentry's JSON verbatim, and is refused in
validateinstead of at parse time:Tolerating the entry at the parse boundary and refusing it where a caller
acts on it as a dependency separates the two things the old behaviour
conflated. A manifest stays readable and editable:
add,removeandinfoleave the entry alone and write it back untouched. An unknown dependency
still can never be silently resolved around:
lock,sync,buildandpublishrefuse by name.The catch-all matches a JSON object, so a
usageentry that is not oneremains a parse error — no future kind can explain that shape. It is the last
variant, as
untaggedrequires.Python gains
InterchangeProjectUsageUnknown(a plain dict) andInterchangeProjectUsageAnyfor reading, which is whatInterchangeProjectInfo.usagenow holds. Writing still takesInterchangeProjectUsage, restricted to the kinds sysand understands, soadd(usage=…)cannot be used to declare one.RELEASE.mdreplaces the gap recorded in the PR below with what nowholds, including the part that does not change: builds released before this
one still fail to parse such a manifest. This only moves the floor. A fourth
usage kind can be introduced once those builds are out of circulation, not
today.
Breaking
InterchangeProjectUsageGgains a variant, so amatchover it in Rustneeds a new arm. The crates are not published, so this is internal.
InterchangeProjectInfo.usageis typedInterchangeProjectUsageAnyon the way out. Code that reads a usage as one of the three known shapes
needs a check for the unknown one — which is the point: that value can now
appear where the call used to raise.
is byte-identical to before.