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
33 changes: 23 additions & 10 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,28 @@ it into an Android APK. The same source becomes both.
- `src/feldspar/` is generated on every build: the typed client, `hooks.ts` (the React scaffold's
`useX` / `useCreateX` / `useUpdateX` / `useDeleteX` hooks) and `config.ts`.
- The web bundle comes from `expo export --platform web`.
- The APK comes from `expo prebuild` plus `./gradlew assembleRelease`.
- The Android package is `com.feldspar.<subdomain>`.
- The APK comes from `expo prebuild` plus Gradle, as a release or a debug build; both carry
their JavaScript, and native code for 64-bit devices only (`arm64-v8a`, `x86_64`).
- **Android app settings** ("Android app" card in the application form): app ID, version, icon,
release or debug, and APK or AAB. Release builds can be signed with your own keystore (the
password is a secret, never written into the project); **Generate a keystore** creates one in
the file store. Bad values fail only the Android build.
- **CSP**: React's policy plus `style-src 'unsafe-inline'`, because react-native-web's
`StyleSheet` writes a `<style>` element at run time without a nonce. `script-src` stays strict.
- **Build targets (generic)**: a declared framework can offer builds that produce a file (rather
than a bundle to serve) next to its web bundle:
`targets: { android: { label, command, artifact, env, requires } }`.
- `artifact` is a path template over the framework's settings.
- `env` holds extra environment variables for the build; a blank value falls back to the
server's own.
- `requires` lists what the machine needs: an environment variable (optionally an existing
directory), a command on `PATH`, or an operating system, each with a hint on how to fix it.
Unmet requirements are shown on the target's button and refuse the build before anything runs.
`targets: { android: { label, command, artifact, env, requires, options, operations } }`.
- `command`, `artifact` and `env` are templates over the framework's settings; a blank `env`
value falls back to the server's own.
- `options` are the target's own settings, shown in a card of their own in the form.
- `operations` are buttons under those settings that run the module's code; the server
writes the files it returns into the store and saves the settings.
- `requires` lists what the machine needs (an environment variable or directory, a command, an
operating system), checked before a build starts and shown on its button.
- A malformed declaration is refused when the module loads.
- **Settings**: a setting can be shown conditionally (v1's `showIf`) or pick a file of given
types from the app's store. An application's secret settings are masked in the admin API and
kept on save.
- **Target builds run as background jobs**: `POST /api/applications/{id}/targets/{target}/build`
starts a build and answers at once; `GET` on the same path reports `running`, `succeeded` (with
artifact and size) or `failed`.
Expand All @@ -37,7 +45,7 @@ it into an Android APK. The same source becomes both.
lock; Gradle runs outside it.
- The log is written as the build runs to `<project>/build-logs/<target>-<timestamp>.log`, and
the last 10 per target are kept.
- The admin sidebar has a **Build Android APK** button under Build. It polls until the build is
- The admin sidebar has a **Build Android app** button under Build. It polls until the build is
done, picks up a build still running after a reload, and its toast names the APK and the log.
- **Android toolchain**: `ANDROID_HOME` and `JAVA_HOME` are the module's own settings
(Settings → Modules → React Native), passed to the APK build. The shell that started the server
Expand All @@ -61,6 +69,11 @@ it into an Android APK. The same source becomes both.
anonymous.
- Known gaps are listed in TODO.md (RN.6–RN.12).

## Fix: the file manager downloads large files

Download now links to a new route, `GET /download/{store}/{*path}`, which serves the file as
is, instead of loading it as base64 inside JSON, which failed for files as large as an APK.

## Fix: Reinstall picks up an edited local or bundled module

npm kept the copy it had made earlier (`--install-links`) because the name and version were
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

7 changes: 7 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,13 @@ Outside the analytics milestones. See CHANGELOG for what each item covers.
- [ ] RN.12 A build dropped while the server runs (a future "cancel build", or a server killed
on its own) kills only `npm`, not the `sh` / `gradlew` / Gradle under it: run the command
in its own process group and kill the group.
- [x] RN.13 The APK's own settings (application ID, version, icon from the app's file store,
debug or release) as a target's `options`, shown in the application form under the target.
- [x] RN.14 Release signing with the admin's own keystore (file from the store, alias, password
as a secret), shown only for a release build once "Sign with your own keystore" is ticked.
- [x] RN.15 "Generate a keystore": a target operation that makes one in the file store and fills
in the signing settings; warns when the store is a git repository.
- [ ] RN.16 Keep secrets such as a generated keystore out of a git file store's commits.

# Not in a milestone yet

Expand Down
107 changes: 107 additions & 0 deletions crates/sc-api/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,28 @@ pub fn admin_endpoints() -> EndpointSet {
.auth(AuthRequirement::admin()),
);

// The files of a kind in a store, by path — what a setting that names a file
// (`store_files:png,jpg`, an app icon or a keystore) offers as its choices.
// Dependency and generated directories are skipped, so a project's
// `node_modules` does not bury the one icon the admin is looking for.
set.register(
Endpoint::new(
"listStoreFiles",
Method::Get,
api()
.lit("file-stores")
.param("store", ValueType::Text)
.lit("files-by-type"),
)
// Comma-separated, without dots: `png,jpg,jpeg`.
.query([QueryParam::new("extensions", ValueType::Text)])
.output(TypeSchema::struct_of([
StructField::new("paths", TypeSchema::array(TypeSchema::text())),
StructField::new("truncated", TypeSchema::bool()),
]))
.auth(AuthRequirement::admin()),
);

// Search a store's text files, server-side. This is the endpoint the IDE's
// find-in-files runs on (§12.1): walking the tree through the filesystem
// provider is one request per directory, and the same walk done where the
Expand Down Expand Up @@ -1748,6 +1770,41 @@ pub fn admin_endpoints() -> EndpointSet {
.auth(AuthRequirement::admin()),
);

// Run an operation a target declares — what a button under the target's
// settings does, such as generating a signing keystore. The module makes the
// files and settings; the server writes the files into the application's
// store (never over an existing one) and saves the settings on it. The body
// is the form's current framework settings, unsaved edits included, so the
// module sees what the admin sees.
set.register(
Endpoint::new(
"runApplicationTargetOperation",
Method::Post,
api()
.lit("applications")
.param("id", ValueType::Uuid)
.lit("targets")
.param("target", ValueType::Text)
.lit("operations")
.param("operation", ValueType::Text),
)
.input(TypeSchema::struct_of([StructField::new(
"config",
TypeSchema::json(),
)]))
.output(TypeSchema::struct_of([
StructField::new("message", TypeSchema::text()),
StructField::new("store", TypeSchema::text()),
StructField::new("files", TypeSchema::array(TypeSchema::text())),
// Whether the store is a git repository, whose next commit would
// carry the files.
StructField::new("git_repo", TypeSchema::bool()),
// The settings it set, as now stored, secrets masked.
StructField::new("settings", TypeSchema::json()),
]))
.auth(AuthRequirement::admin()),
);

// The latest build of a target, running or finished — what the UI polls, and
// what it asks after a reload to find a build still running. A 404 when this
// process has not built that target since it started.
Expand Down Expand Up @@ -5627,6 +5684,46 @@ fn framework_info_schema() -> TypeSchema {
// because on those pickers it offers to create a local store rather
// than making the admin leave the form to define one first.
StructField::new("file_store_settings", TypeSchema::array(TypeSchema::text())),
// The settings whose value is a file in the application's store (an
// app icon, a keystore), each with the extensions it accepts: the form
// fills their choices from `listStoreFiles` for the store the
// application names.
StructField::new(
"file_settings",
TypeSchema::array(TypeSchema::struct_of([
StructField::new("name", TypeSchema::text()),
StructField::new("extensions", TypeSchema::array(TypeSchema::text())),
])),
),
// The builds the framework offers beside its web bundle, each with the
// settings that configure it alone. Those settings are in `config_spec`
// too; the form shows them under their target instead of among the
// framework's own.
StructField::new(
"targets",
TypeSchema::array(TypeSchema::struct_of([
StructField::new("name", TypeSchema::text()),
StructField::new("label", TypeSchema::text()),
StructField::new("options", TypeSchema::array(TypeSchema::text())),
// What the module does for the target on request: a button
// each, shown while its `show_if` holds.
StructField::new(
"operations",
TypeSchema::array(TypeSchema::struct_of([
StructField::new("name", TypeSchema::text()),
StructField::new("label", TypeSchema::text()),
StructField::new("description", TypeSchema::text()),
StructField::new(
"show_if",
TypeSchema::array(TypeSchema::struct_of([
StructField::new("name", TypeSchema::text()),
StructField::new("values", TypeSchema::array(TypeSchema::json())),
])),
),
])),
),
])),
),
])
}

Expand Down Expand Up @@ -6083,6 +6180,16 @@ fn form_field_schema() -> TypeSchema {
// The language this value is source code in (`"javascript"`), or null for
// a setting that is not code: the form renders a code editor for it.
StructField::new("code_language", TypeSchema::optional(TypeSchema::text())),
// When the setting applies: every named setting holds one of its
// values. Empty means always. The form hides a setting that does not
// apply, and the server does not require it.
StructField::new(
"show_if",
TypeSchema::array(TypeSchema::struct_of([
StructField::new("name", TypeSchema::text()),
StructField::new("values", TypeSchema::array(TypeSchema::json())),
])),
),
])
}

Expand Down
18 changes: 13 additions & 5 deletions crates/sc-app/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,19 +644,25 @@ pub async fn build_code_framework(

/// One build target an application's framework offers — what a button says and
/// what a build request names.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct TargetInfo {
/// The key — `android`.
pub name: String,
/// The label — `Android APK`.
pub label: String,
/// The framework settings that configure this target alone (see
/// [`crate::TargetTemplate::options`]).
pub options: Vec<String>,
/// What the module can do for the target on request (see
/// [`crate::TargetOperation`]).
pub operations: Vec<crate::TargetOperation>,
}

/// The build targets `app`'s framework declares, in declaration order. Empty for
/// the built-in frameworks, which only serve.
pub fn app_build_targets(app: &Application) -> Vec<TargetInfo> {
/// The build targets framework `name` declares, in declaration order. Empty for
/// the built-in frameworks and for a name nothing declares.
pub fn framework_build_targets(name: &str) -> Vec<TargetInfo> {
installed_frameworks()
.find(&app.framework.name)
.find(name)
.map(|decl| {
decl.targets
.iter()
Expand All @@ -667,6 +673,8 @@ pub fn app_build_targets(app: &Application) -> Vec<TargetInfo> {
} else {
t.label.clone()
},
options: t.options.clone(),
operations: t.operations.clone(),
})
.collect()
})
Expand Down
Loading
Loading