Say on the card when a project's own file was ignored
An unknown field discards the whole declaration rather than the key it did not recognise, so a machine that takes a project's commit before it takes a dev-updater that understands it loses that project's components, strip, staleWhen and resources at once. Nothing looks broken afterwards: the card becomes a project that declares nothing, which is the ordinary case, and the only thing saying otherwise was a line in this server's log on a machine the person holding the phone cannot read. project_config now answers a ProjectFile carrying the parse error beside the declaration, and AppEntry::declaration_state returns both halves from one read -- the manifest wants both, and the file is on that path. The card draws its own sentence about what it means and what to do, then the parser's own words, which name the file, the position and the offending field; that half is selectable, since the fix happens on the other machine. Nothing is blocked by it. What was already accepted is what runs, so an unrelated typo cannot stop a project that was working -- asserted in the test, along with the error naming both the field and the file. Seen on the emulator both ways: a project whose file gained a field from the future says so under its action row, and the whole message goes when the file parses again. Raised by the tdep-survey session after its alsoWatch commit hit exactly this against the older binary running here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c7b3af4861
commit
0b7164bb30
8 files changed
+209
-27
No files matched your search
@@ -255,6 +255,26 @@ mutable at runtime from the phone.
|
|||||||
that silence. It is part of the acceptance gate like every other
|
that silence. It is part of the acceptance gate like every other
|
||||||
declared field -- `same_declaration` destructures exhaustively, which is
|
declared field -- `same_declaration` destructures exhaustively, which is
|
||||||
what forced the decision when the field was added.
|
what forced the decision when the field was added.
|
||||||
|
- **A declaration that cannot be read says so on the card.** RON's
|
||||||
|
`deny_unknown_fields` discards the *whole* declaration rather than the
|
||||||
|
key it did not recognise, so a machine that takes a project's commit
|
||||||
|
before it takes a dev-updater new enough to understand it loses that
|
||||||
|
project's components, `strip`, `staleWhen` and resources at once -- and
|
||||||
|
the card does not look broken, it looks like a project that declares
|
||||||
|
nothing, which is the ordinary case. That was the only difference
|
||||||
|
between the two states: a line in this server's log, on a machine the
|
||||||
|
person holding the phone cannot read. `config::project_config` now
|
||||||
|
answers a `ProjectFile` carrying the parse error beside the
|
||||||
|
declaration, `AppEntry::declaration_state` returns both halves from one
|
||||||
|
read (the manifest wants both, and the file is on that path), and the
|
||||||
|
card draws its own sentence about what it means followed by the
|
||||||
|
parser's own words -- selectable, since the fix happens on the other
|
||||||
|
machine. Nothing is *blocked* by it: what was already accepted is what
|
||||||
|
runs, so an unrelated typo cannot stop a project that was working.
|
||||||
|
Worst on the self entry, where `reconcile_self` derives the row from
|
||||||
|
that file at every startup: unreadable, and this server's own card
|
||||||
|
silently loses its server component and its build commands rather than
|
||||||
|
merely stopping noticing changes.
|
||||||
- **The debug keystore is not `~/.android/debug.keystore`** -- that is the
|
- **The debug keystore is not `~/.android/debug.keystore`** -- that is the
|
||||||
last of five places, and `sdk::android_prefs_dirs` resolves it the way
|
last of five places, and `sdk::android_prefs_dirs` resolves it the way
|
||||||
AGP does, because the only thing that makes it the right key is being
|
AGP does, because the only thing that makes it the right key is being
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ data class ManifestEntry(
|
|||||||
// Null once accepted, or for a project that asks for nothing. While it
|
// Null once accepted, or for a project that asks for nothing. While it
|
||||||
// is set the server runs no build step for this app at all.
|
// is set the server runs no build step for this app at all.
|
||||||
val pendingDeclaration: String?,
|
val pendingDeclaration: String?,
|
||||||
|
// Why this project's own .dev-updater.ron was ignored, when it could
|
||||||
|
// not be read. Null covers both "it parsed" and "there is no file",
|
||||||
|
// which are the same thing to a reader -- what this exists for is the
|
||||||
|
// third case, where the file is there, says something, and none of it
|
||||||
|
// is being used. An unknown field discards the whole declaration
|
||||||
|
// rather than the key it did not recognise, so the card silently
|
||||||
|
// becomes an emptier project instead of a broken one.
|
||||||
|
val declarationError: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
// A project's checkout, all read locally on the server.
|
// A project's checkout, all read locally on the server.
|
||||||
@@ -340,6 +348,7 @@ private fun readEntry(entry: JSONObject): ManifestEntry {
|
|||||||
checkError = entry.optString("checkError").ifEmpty { null },
|
checkError = entry.optString("checkError").ifEmpty { null },
|
||||||
checkPending = entry.optBoolean("checkPending", false),
|
checkPending = entry.optBoolean("checkPending", false),
|
||||||
pendingDeclaration = entry.optString("pendingDeclaration").ifEmpty { null },
|
pendingDeclaration = entry.optString("pendingDeclaration").ifEmpty { null },
|
||||||
|
declarationError = entry.optString("declarationError").ifEmpty { null },
|
||||||
components =
|
components =
|
||||||
(0 until (components?.length() ?: 0)).map { j ->
|
(0 until (components?.length() ?: 0)).map { j ->
|
||||||
val component = components!!.getJSONObject(j)
|
val component = components!!.getJSONObject(j)
|
||||||
|
|||||||
@@ -1919,6 +1919,29 @@ private fun AppCard(
|
|||||||
OutputText(reason, style = MaterialTheme.typography.bodySmall)
|
OutputText(reason, style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Said here, with the project's identity, because it is about
|
||||||
|
// this project's own file rather than about any one component
|
||||||
|
// -- and because what it costs is invisible further down: the
|
||||||
|
// file is ignored *whole*, so the card quietly becomes an
|
||||||
|
// emptier project, with components, strip and resources it
|
||||||
|
// asked for simply not there. Nothing else on the card can say
|
||||||
|
// that, since a project that declares nothing looks exactly
|
||||||
|
// the same.
|
||||||
|
//
|
||||||
|
// Two parts, and the split is the usual one: this app's own
|
||||||
|
// sentence for what it means and what to do, then the parser's
|
||||||
|
// own words -- which name the file, the line and the field --
|
||||||
|
// selectable, because fixing it happens on the other machine.
|
||||||
|
entry.declarationError?.let { reason ->
|
||||||
|
Text(
|
||||||
|
"This project's own file was ignored, so nothing it asks for is being " +
|
||||||
|
"read. Fix it on the build machine, or update Dev Updater if the file " +
|
||||||
|
"uses something newer than it knows.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = ActionTone.Caution.color,
|
||||||
|
)
|
||||||
|
OutputText(reason, style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1411,7 +1411,11 @@ mod tests {
|
|||||||
let project = clone.clone();
|
let project = clone.clone();
|
||||||
state.pull_and_build(
|
state.pull_and_build(
|
||||||
false,
|
false,
|
||||||
move || crate::config::project_config(&project).matches_accepted(&accepted, None),
|
move || {
|
||||||
|
crate::config::project_config(&project)
|
||||||
|
.declaration
|
||||||
|
.matches_accepted(&accepted, None)
|
||||||
|
},
|
||||||
Arc::new(|_: &str, _: String| {}),
|
Arc::new(|_: &str, _: String| {}),
|
||||||
);
|
);
|
||||||
while state.status().building {
|
while state.status().building {
|
||||||
|
|||||||
+43
-11
@@ -756,21 +756,53 @@ pub fn render_request(components: &[Component]) -> Result<String, ron::Error> {
|
|||||||
wg_app_link::format::render(&Request { components })
|
wg_app_link::format::render(&Request { components })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A project's own file: what it says, and why it could not be read.
|
||||||
|
///
|
||||||
|
/// Two fields rather than a `Result` because the caller needs both halves
|
||||||
|
/// at once. Every caller carries on with an empty declaration -- one
|
||||||
|
/// project's typo must not take the whole app list down -- and the card
|
||||||
|
/// still has to be able to say that is what happened, which a discarded
|
||||||
|
/// error cannot do.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct ProjectFile {
|
||||||
|
/// Empty for a project that says nothing *and* for one whose file
|
||||||
|
/// could not be read. [`Self::error`] is what tells those apart.
|
||||||
|
pub declaration: Declaration,
|
||||||
|
/// Why the file was ignored, in the parser's own words -- position,
|
||||||
|
/// the offending field, and what was expected there.
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// What `project` says about itself, or an empty declaration for a project
|
/// What `project` says about itself, or an empty declaration for a project
|
||||||
/// that says nothing -- which is the normal case.
|
/// that says nothing -- which is the normal case.
|
||||||
///
|
///
|
||||||
/// A file that doesn't parse is logged and treated as saying nothing
|
/// A file that doesn't parse is treated as saying nothing rather than as
|
||||||
/// rather than as an error: this runs while rebuilding the entry list, and
|
/// an error, because this runs while rebuilding the entry list. What it is
|
||||||
/// one project's typo must not take the whole app list down with it.
|
/// *not* is silent: an unknown field discards the whole declaration, not
|
||||||
pub fn project_config(project: &Path) -> Declaration {
|
/// the key it did not recognise, so a machine that takes a project's
|
||||||
|
/// commit before it takes a dev-updater that understands it loses that
|
||||||
|
/// project's build steps, its `strip`, its `staleWhen` and its resources
|
||||||
|
/// at once. Nothing about the card looks broken afterwards -- it becomes
|
||||||
|
/// a quieter, emptier project -- which is why the reason travels to the
|
||||||
|
/// phone rather than only to this server's log.
|
||||||
|
pub fn project_config(project: &Path) -> ProjectFile {
|
||||||
let path = project.join(PROJECT_CONFIG_FILE);
|
let path = project.join(PROJECT_CONFIG_FILE);
|
||||||
let Ok(text) = std::fs::read_to_string(&path) else {
|
let Ok(text) = std::fs::read_to_string(&path) else {
|
||||||
return Declaration::default();
|
return ProjectFile::default();
|
||||||
};
|
};
|
||||||
wg_app_link::format::parse(&text).unwrap_or_else(|err| {
|
match wg_app_link::format::parse(&text) {
|
||||||
|
Ok(declaration) => ProjectFile {
|
||||||
|
declaration,
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
tracing::warn!("ignoring {}: {err}", path.display());
|
tracing::warn!("ignoring {}: {err}", path.display());
|
||||||
Declaration::default()
|
ProjectFile {
|
||||||
})
|
declaration: Declaration::default(),
|
||||||
|
error: Some(format!("{}: {err}", path.display())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The inverse of [`expand_tilde`], for paths shown on a phone.
|
/// The inverse of [`expand_tilde`], for paths shown on a phone.
|
||||||
@@ -1060,7 +1092,7 @@ mod tests {
|
|||||||
fn reads_the_build_step_a_project_asks_for() {
|
fn reads_the_build_step_a_project_asks_for() {
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
// The normal case: no file at all, which is not an error.
|
// The normal case: no file at all, which is not an error.
|
||||||
assert!(project_config(dir.path()).components.is_empty());
|
assert!(project_config(dir.path()).declaration.components.is_empty());
|
||||||
|
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
dir.path().join(PROJECT_CONFIG_FILE),
|
dir.path().join(PROJECT_CONFIG_FILE),
|
||||||
@@ -1069,7 +1101,7 @@ mod tests {
|
|||||||
components: [Apk(name: \"app\", build: \"./build-apk.sh\")],\n",
|
components: [Apk(name: \"app\", build: \"./build-apk.sh\")],\n",
|
||||||
)
|
)
|
||||||
.expect("write");
|
.expect("write");
|
||||||
let asked = project_config(dir.path());
|
let asked = project_config(dir.path()).declaration;
|
||||||
assert!(!asked.git_pull);
|
assert!(!asked.git_pull);
|
||||||
let [component] = &asked.components[..] else {
|
let [component] = &asked.components[..] else {
|
||||||
panic!("one component")
|
panic!("one component")
|
||||||
@@ -1087,7 +1119,7 @@ mod tests {
|
|||||||
"components: [Apk(name:",
|
"components: [Apk(name:",
|
||||||
)
|
)
|
||||||
.expect("write");
|
.expect("write");
|
||||||
assert!(project_config(dir.path()).components.is_empty());
|
assert!(project_config(dir.path()).declaration.components.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Both ways of writing a command mean the same argv, and the one with
|
/// Both ways of writing a command mean the same argv, and the one with
|
||||||
|
|||||||
+1
-1
@@ -170,7 +170,7 @@ struct Args {
|
|||||||
/// declaration -- so a bootstrap script cannot drift out of step with them
|
/// declaration -- so a bootstrap script cannot drift out of step with them
|
||||||
/// the way a second copy of the arguments would.
|
/// the way a second copy of the arguments would.
|
||||||
fn drive_own_service(self_project: &Path, subcommand: &str) -> Result<()> {
|
fn drive_own_service(self_project: &Path, subcommand: &str) -> Result<()> {
|
||||||
let declaration = config::project_config(self_project);
|
let declaration = config::project_config(self_project).declaration;
|
||||||
let component = declaration
|
let component = declaration
|
||||||
.components
|
.components
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
+93
-13
@@ -33,6 +33,17 @@ pub const SELF_KEY: &str = "updater";
|
|||||||
const SELF_PACKAGE: &str = "com.example.devupdater";
|
const SELF_PACKAGE: &str = "com.example.devupdater";
|
||||||
pub(crate) const SELF_LABEL: &str = "Dev Updater";
|
pub(crate) const SELF_LABEL: &str = "Dev Updater";
|
||||||
|
|
||||||
|
/// What a project's own `.dev-updater.ron` says as of now, which is two
|
||||||
|
/// separate things a card has to be able to draw differently. See
|
||||||
|
/// [`AppEntry::declaration_state`].
|
||||||
|
pub struct DeclarationState {
|
||||||
|
/// What it asks for that has not been accepted, when that is anything.
|
||||||
|
pub pending: Option<Vec<Component>>,
|
||||||
|
/// Why the file was ignored. `None` covers both "it parsed" and "there
|
||||||
|
/// is no file", which are the same thing to everything downstream.
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// One servable app, resolved from config (or built in, for the self
|
/// One servable app, resolved from config (or built in, for the self
|
||||||
/// entry). Cheap to clone-by-`Arc` and handed to request handlers whole.
|
/// entry). Cheap to clone-by-`Arc` and handed to request handlers whole.
|
||||||
pub struct AppEntry {
|
pub struct AppEntry {
|
||||||
@@ -161,21 +172,43 @@ impl AppEntry {
|
|||||||
/// Cheap enough for the manifest path: one small read per app, beside
|
/// Cheap enough for the manifest path: one small read per app, beside
|
||||||
/// the `git status` that path already runs for each of them.
|
/// the `git status` that path already runs for each of them.
|
||||||
pub fn pending_declaration(&self) -> Option<Vec<Component>> {
|
pub fn pending_declaration(&self) -> Option<Vec<Component>> {
|
||||||
|
self.declaration_state().pending
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Both halves of what this project's own file says right now: what it
|
||||||
|
/// is asking for that nobody has accepted, and why it could not be
|
||||||
|
/// read at all.
|
||||||
|
///
|
||||||
|
/// One read answering both, because the manifest wants both and the
|
||||||
|
/// file is on that path.
|
||||||
|
///
|
||||||
|
/// The two are not alternatives. A file that does not parse is asking
|
||||||
|
/// for nothing -- so `pending` is `None`, and the card would otherwise
|
||||||
|
/// be indistinguishable from a project that declares nothing at all,
|
||||||
|
/// which is the ordinary case. It is the `error` that separates them,
|
||||||
|
/// and without it the only sign was a line in this server's log.
|
||||||
|
pub fn declaration_state(&self) -> DeclarationState {
|
||||||
// This server's own project is accepted by construction (see
|
// This server's own project is accepted by construction (see
|
||||||
// `reconcile_self`), and the row saying so is only re-derived at
|
// `reconcile_self`), and the row saying so is only re-derived at
|
||||||
// startup -- so between a pull that changes the declaration and the
|
// startup -- so between a pull that changes the declaration and the
|
||||||
// restart that follows it, the two can differ without that meaning
|
// restart that follows it, the two can differ without that meaning
|
||||||
// anything is waiting to be accepted.
|
// anything is waiting to be accepted. An unreadable file is still
|
||||||
if self.built_in {
|
// worth saying, and matters more here than anywhere: this row's
|
||||||
return None;
|
// components come *from* that file at startup, so a declaration
|
||||||
|
// this server cannot parse is a card that has lost its server
|
||||||
|
// component and its build commands rather than one that has
|
||||||
|
// stopped noticing changes.
|
||||||
|
let file = crate::config::project_config(&self.project_path);
|
||||||
|
let pending = (!self.built_in
|
||||||
|
&& !file.declaration.components.is_empty()
|
||||||
|
&& !file
|
||||||
|
.declaration
|
||||||
|
.matches_accepted(&self.components, self.resources.as_ref()))
|
||||||
|
.then_some(file.declaration.components);
|
||||||
|
DeclarationState {
|
||||||
|
pending,
|
||||||
|
error: file.error,
|
||||||
}
|
}
|
||||||
let declared = crate::config::project_config(&self.project_path);
|
|
||||||
if declared.components.is_empty()
|
|
||||||
|| declared.matches_accepted(&self.components, self.resources.as_ref())
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(declared.components)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This project's resources, as something to read, when it declares
|
/// This project's resources, as something to read, when it declares
|
||||||
@@ -540,7 +573,7 @@ impl AppState {
|
|||||||
// project is still added -- there is simply nothing installed for
|
// project is still added -- there is simply nothing installed for
|
||||||
// it yet, which is the honest answer and is what lets a project be
|
// it yet, which is the honest answer and is what lets a project be
|
||||||
// added in order to run the build that produces its first APK.
|
// added in order to run the build that produces its first APK.
|
||||||
let declared = crate::config::project_config(&project);
|
let declared = crate::config::project_config(&project).declaration;
|
||||||
let (label, package) = match discover::find_apks(&project).into_iter().next() {
|
let (label, package) = match discover::find_apks(&project).into_iter().next() {
|
||||||
Some(apk) => {
|
Some(apk) => {
|
||||||
let info = crate::apkinfo::read(&apk.path)?;
|
let info = crate::apkinfo::read(&apk.path)?;
|
||||||
@@ -647,7 +680,7 @@ impl AppState {
|
|||||||
let entry = self
|
let entry = self
|
||||||
.entry(key)
|
.entry(key)
|
||||||
.with_context(|| format!("no app named {key}"))?;
|
.with_context(|| format!("no app named {key}"))?;
|
||||||
let declared = crate::config::project_config(&entry.project_path);
|
let declared = crate::config::project_config(&entry.project_path).declaration;
|
||||||
if declared.components.is_empty() {
|
if declared.components.is_empty() {
|
||||||
bail!(
|
bail!(
|
||||||
"{} no longer asks for anything -- its {} is gone or unreadable",
|
"{} no longer asks for anything -- its {} is gone or unreadable",
|
||||||
@@ -712,7 +745,7 @@ impl AppState {
|
|||||||
/// doing the gating, so whatever arrives is already trusted by the time it
|
/// doing the gating, so whatever arrives is already trusted by the time it
|
||||||
/// could run.
|
/// could run.
|
||||||
fn reconcile_self(config: &mut Config, self_project: &Path) -> bool {
|
fn reconcile_self(config: &mut Config, self_project: &Path) -> bool {
|
||||||
let declared = crate::config::project_config(self_project);
|
let declared = crate::config::project_config(self_project).declaration;
|
||||||
// Asked of the project itself, not of its parent. `git status` answers
|
// Asked of the project itself, not of its parent. `git status` answers
|
||||||
// from any subdirectory, so the two agree whenever `app/` is really
|
// from any subdirectory, so the two agree whenever `app/` is really
|
||||||
// there -- and differ in the one case that matters: a working directory
|
// there -- and differ in the one case that matters: a working directory
|
||||||
@@ -1168,6 +1201,53 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A file this server cannot read says nothing, which is exactly what
|
||||||
|
/// a project with no file says -- so the reason has to travel
|
||||||
|
/// separately, or the two are the same card.
|
||||||
|
///
|
||||||
|
/// The shape that produces it: an unknown field discards the *whole*
|
||||||
|
/// declaration rather than the key it did not recognise, so a machine
|
||||||
|
/// that takes a project's commit before it takes a dev-updater new
|
||||||
|
/// enough to understand it loses that project's build steps, `strip`,
|
||||||
|
/// `staleWhen` and resources at once, with nothing on screen changed
|
||||||
|
/// but the absence.
|
||||||
|
#[test]
|
||||||
|
fn a_declaration_that_cannot_be_read_says_so_rather_than_saying_nothing() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
write_request(dir.path(), "./build.sh");
|
||||||
|
let entry = entry_for(dir.path(), asking_for("./build.sh"));
|
||||||
|
let state = entry.declaration_state();
|
||||||
|
assert_eq!(state.pending, None, "accepted to begin with");
|
||||||
|
assert_eq!(state.error, None, "and readable");
|
||||||
|
|
||||||
|
// A field from a later version of dev-updater than this one.
|
||||||
|
std::fs::write(
|
||||||
|
dir.path().join(crate::config::PROJECT_CONFIG_FILE),
|
||||||
|
"components: [Apk(name: \"app\", build: \"./build.sh\", fromTheFuture: true)],\n",
|
||||||
|
)
|
||||||
|
.expect("write");
|
||||||
|
|
||||||
|
let state = entry.declaration_state();
|
||||||
|
assert_eq!(
|
||||||
|
state.pending, None,
|
||||||
|
"a file that parsed into nothing is asking for nothing",
|
||||||
|
);
|
||||||
|
let error = state.error.expect("the reason it was ignored");
|
||||||
|
assert!(
|
||||||
|
error.contains("fromTheFuture"),
|
||||||
|
"the field that stopped it: {error}",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
error.contains(crate::config::PROJECT_CONFIG_FILE),
|
||||||
|
"and which file it was in: {error}",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
entry.components,
|
||||||
|
asking_for("./build.sh"),
|
||||||
|
"what was already accepted still runs -- this is about noticing, not about stopping",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// A checkout laid out the way this repository is: `<repo>/app` is the
|
/// A checkout laid out the way this repository is: `<repo>/app` is the
|
||||||
/// self entry's project, and `<repo>` is what gets pulled.
|
/// self entry's project, and `<repo>` is what gets pulled.
|
||||||
fn self_checkout(root: &Path) -> PathBuf {
|
fn self_checkout(root: &Path) -> PathBuf {
|
||||||
|
|||||||
+15
-1
@@ -546,6 +546,18 @@ struct ManifestApp {
|
|||||||
/// something the keys don't say.
|
/// something the keys don't say.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pending_declaration: Option<String>,
|
pending_declaration: Option<String>,
|
||||||
|
/// Why this project's own `.dev-updater.ron` was ignored, when it
|
||||||
|
/// could not be read.
|
||||||
|
///
|
||||||
|
/// Sent because the card is otherwise indistinguishable from a project
|
||||||
|
/// that declares nothing, which is the ordinary case: an unknown field
|
||||||
|
/// discards the whole declaration rather than the key it did not
|
||||||
|
/// recognise, so a machine that takes a project's commit before it
|
||||||
|
/// takes a dev-updater that understands it quietly becomes a different,
|
||||||
|
/// emptier project. Nothing looks broken, and the only sign was a line
|
||||||
|
/// in a log on a machine the person holding the phone cannot read.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
declaration_error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One build found under a project. Which of them a device wants is that
|
/// One build found under a project. Which of them a device wants is that
|
||||||
@@ -643,7 +655,8 @@ async fn describe(state: &Arc<AppState>, entry: &AppEntry) -> Result<ManifestApp
|
|||||||
// Read before the components, because each of them reports whether it
|
// Read before the components, because each of them reports whether it
|
||||||
// can be built and none of them can while this is set: an unaccepted
|
// can be built and none of them can while this is set: an unaccepted
|
||||||
// declaration is exactly the command that must not run.
|
// declaration is exactly the command that must not run.
|
||||||
let pending = entry.pending_declaration();
|
let declaration = entry.declaration_state();
|
||||||
|
let pending = declaration.pending;
|
||||||
let mut components = Vec::with_capacity(entry.components.len());
|
let mut components = Vec::with_capacity(entry.components.len());
|
||||||
for component in &entry.components {
|
for component in &entry.components {
|
||||||
components.push(
|
components.push(
|
||||||
@@ -693,6 +706,7 @@ async fn describe(state: &Arc<AppState>, entry: &AppEntry) -> Result<ManifestApp
|
|||||||
pending_declaration: pending
|
pending_declaration: pending
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|components| crate::config::render_request(components).ok()),
|
.and_then(|components| crate::config::render_request(components).ok()),
|
||||||
|
declaration_error: declaration.error,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user