Repository: zng-ui/zng (language: rust)

Issue:
Implement licenses collection
We depend on multiple licenses that must be included in the about page of distributed binaries.

- Use `cargo about` to collect a JSON.
- Add a view API to get the licenses used by the prebuilt.
- Add an app command that renders the licenses.


Discussion / hints:

Fix patch:
diff --git a/.cargo/about.toml b/.cargo/about.toml
new file mode 100644
index 000000000..5528b0ef2
--- /dev/null
+++ b/.cargo/about.toml
@@ -0,0 +1,19 @@
+# cargo about generate -c .cargo/about.toml --format json --workspace --all-features
+
+accepted = [
+    "Apache-2.0",
+    "MIT",
+    "MPL-2.0",
+    "Unicode-DFS-2016",
+    "BSL-1.0",
+    "BSD-2-Clause",
+    "BSD-3-Clause",
+    "ISC",
+    "Zlib",
+    "CC0-1.0",
+]
+
+ignore-build-dependencies = true
+ignore-dev-dependencies = true
+filter-noassertion = true
+private = { ignore = true }
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0677aa25d..3fd07f47b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,6 +21,10 @@ jobs:
   check:
     runs-on: ubuntu-latest
     steps:
+      - name: install cargo-about
+        uses: baptiste0928/cargo-install@v3
+        with:
+          crate: cargo-about
       - uses: actions/checkout@v4
       - uses: Swatinem/rust-cache@v2
       - run: cargo do version --verbose
@@ -28,15 +32,24 @@ jobs:
       - run: cargo do check
   doc:
     runs-on: ubuntu-latest
+    needs: [check]
     steps:
+      - name: install cargo-about
+        uses: baptiste0928/cargo-install@v3
+        with:
+          crate: cargo-about
       - uses: actions/checkout@v4
       - uses: Swatinem/rust-cache@v2
       - run: cargo do doc
       - run: cargo clean
   test:
     runs-on: ubuntu-latest
-    needs: []
+    needs: [check]
     steps:
+    - name: install cargo-about
+      uses: baptiste0928/cargo-install@v3
+      with:
+        crate: cargo-about
     - name: install cargo-nextest
       uses: taiki-e/install-action@v2
       with:
@@ -47,20 +60,30 @@ jobs:
     - run: cargo clean
   test-doc:
     runs-on: ubuntu-latest
+    needs: [check]
     steps:
+      - name: install cargo-about
+        uses: baptiste0928/cargo-install@v3
+        with:
+          crate: cargo-about
       - uses: actions/checkout@v4
       - uses: Swatinem/rust-cache@v2
       - run: cargo do test --doc
       - run: cargo clean
   test-macro:
     runs-on: ubuntu-latest
+    needs: [check]
     steps:
+      - name: install cargo-about
+        uses: baptiste0928/cargo-install@v3
+        with:
+          crate: cargo-about
       - uses: actions/checkout@v4
       - uses: Swatinem/rust-cache@v2
       - run: cargo do test --macro --all
       - run: cargo clean
   test-all:
     runs-on: ubuntu-latest
-    needs: [check, doc, test, test-doc, test-macro]
+    needs: [doc, test, test-doc, test-macro]
     steps:
       - run: exit 0
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e3601f71e..f52a62cc0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -75,6 +75,7 @@ jobs:
         env:
           DAV1D_DIR: dav1d_dir
           LIB_PATH: lib/x86_64-linux-gnu
+        # dav1d version must be set in zng-view/build.rs too.
         run: |
           git clone --branch 1.3.0 --depth 1 https://code.videolan.org/videolan/dav1d.git
           cd dav1d
diff --git a/.vscode/settings.json b/.vscode/settings.json
index e78a9d4f7..1966a834b 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -110,6 +110,7 @@
         "mul",
         "nalt",
         "nlck",
+        "noassertion",
         "numlock",
         "onum",
         "ordn",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76310f0fc..340b853d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,13 @@
 # Unpublished
 
 * Fix docs.rs build for `zng` and `zng-wgt-material-icons`.
-* 
+* Add AVIF support in the prebuilt.
+* Implement prebuilt compression, prebuilt now depends on `tar`.
+* Implement `PartialOrd, Ord` for `Txt`.
+* Add crate `zng-tp-licenses` for collecting and bundling licenses.
+* Add `third_party_licenses` on view API that provides prebuilt bundled licenses.
+* Add `zng::third_party` with service and types for aggregating third party license info.
+    - Includes a default impl of `OPEN_LICENSES_CMD` that shows bundled licenses.
 
 # 0.3.0
 
diff --git a/Cargo.toml b/Cargo.toml
index 895bb7dd4..fda8ca642 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,7 @@ members = [
     "zng-color-proc-macros",
     "zng-color",
     "zng-time",
+    "zng-tp-licenses",
 
     # view:
     "zng-view-api",
diff --git a/tools/do-tasks/src/main.rs b/tools/do-tasks/src/main.rs
index fa6671408..5432cc131 100644
--- a/tools/do-tasks/src/main.rs
+++ b/tools/do-tasks/src/main.rs
@@ -47,6 +47,7 @@ fn install(mut args: Vec<&str>) {
         cmd("rustup", &["component", "add", "clippy"], &[]);
         cmd("cargo", &["install", "cargo-expand"], &[]);
         cmd("cargo", &["install", "cargo-asm"], &[]);
+        cmd("cargo", &["install", "cargo-about"], &[]);
     } else {
         println(f!(
             "Install cargo binaries used by `do` after confirmation.\n  ACCEPT:\n   {} install --accept\n\n  TO RUN:",
@@ -56,7 +57,7 @@ fn install(mut args: Vec<&str>) {
         println("   rustup component add rustfmt");
         println("   rustup component add clippy");
         println("   cargo install cargo-expand");
-        println("   cargo install cargo-asm");
+        println("   cargo install cargo-about");
     }
 }
 
@@ -568,7 +569,11 @@ fn fmt(mut args: Vec<&str>) {
 // do check, c
 //    Runs clippy on the workspace.
 fn check(args: Vec<&str>) {
-    cmd("cargo", &["clippy", "--no-deps", "--tests", "--workspace", "--examples"], &args);
+    cmd(
+        "cargo",
+        &["clippy", "--no-deps", "--tests", "--workspace", "--examples", "--all-features"],
+        &args,
+    );
 }
 
 // do build, b [-e, --example] [--examples] [-t, --timings] [--release-lto] [-Z*] [<cargo-build-args>]
@@ -645,7 +650,11 @@ fn prebuild(mut args: Vec<&str>) {
         "prebuild"
     };
 
-    cmd("cargo", &["build", "-p", "zng-view", "--profile", profile], &args);
+    cmd(
+        "cargo",
+        &["build", "-p", "zng-view", "--profile", profile, "--features", "bundle_licenses"],
+        &args,
+    );
 
     let files = cdylib_files(format!("target/{}/zng_view", if profile == "dev" { "debug" } else { profile }));
 
diff --git a/tools/do-tasks/src/util.rs b/tools/do-tasks/src/util.rs
index 557aa034f..d22d2f239 100644
--- a/tools/do-tasks/src/util.rs
+++ b/tools/do-tasks/src/util.rs
@@ -533,7 +533,7 @@ pub fn publish_members() -> Vec<PublishMember> {
                     let line = line.trim();
                     if line == "[package]" {
                         section = Section::Package;
-                    } else if line == "[dependencies]" {
+                    } else if line.ends_with("dependencies]") {
                         section = Section::Dependencies;
                     } else if line.starts_with('[') && line.ends_with(']') {
                         section = Section::Other;
@@ -674,7 +674,7 @@ impl PublishMember {
             let line_edit = line.trim();
             if line_edit == "[package]" {
                 section = Section::Package;
-            } else if line_edit == "[dependencies]" {
+            } else if line_edit.ends_with("dependencies]") {
                 section = Section::Dependencies;
             } else if line_edit.starts_with('[') && line_edit.ends_with(']') {
                 section = Section::Other;
diff --git a/tools/do-tasks/src/version_doc_sync.rs b/tools/do-tasks/src/version_doc_sync.rs
index f0bfd405a..4373271e8 100644
--- a/tools/do-tasks/src/version_doc_sync.rs
+++ b/tools/do-tasks/src/version_doc_sync.rs
@@ -16,6 +16,7 @@ const CRATE_FILES: &[(&str, &[&str])] = &[
         ],
     ),
     ("zng-view", &["zng-view/src/lib.rs"]),
+    ("zng-tp-licenses", &["zng/src/third_party.rs"]),
 ];
 
 pub fn check() {
diff --git a/zng-app/Cargo.toml b/zng-app/Cargo.toml
index f53168919..5bd18720d 100644
--- a/zng-app/Cargo.toml
+++ b/zng-app/Cargo.toml
@@ -79,6 +79,7 @@ zng-app-proc-macros = { path = "../zng-app-proc-macros", version = "0.2.3" }
 
 zng-unique-id = { path = "../zng-unique-id", version = "0.2.3" }
 zng-handle = { path = "../zng-handle", version = "0.2.2" }
+zng-tp-licenses = { path = "../zng-tp-licenses", version = "0.2.0" }
 zng-clone-move = { path = "../zng-clone-move", version = "0.2.2" }
 zng-app-context = { path = "../zng-app-context", version = "0.3.0" }
 zng-time = { path = "../zng-time", version = "0.2.3" }
diff --git a/zng-app/README.md b/zng-app/README.md
index e025ef2c5..b734044de 100644
--- a/zng-app/README.md
+++ b/zng-app/README.md
@@ -1,1 +1,2 @@
-This crate is part of the [`zng`](https://github.com/zng-ui/zng) project.
\ No newline at end of file
+This crate is part of the [`zng`](https://github.com/zng-ui/zng) project.
+
diff --git a/zng-app/src/lib.rs b/zng-app/src/lib.rs
index 3719eaf8f..7595509c3 100644
--- a/zng-app/src/lib.rs
+++ b/zng-app/src/lib.rs
@@ -28,6 +28,7 @@ pub mod event;
 pub mod handler;
 pub mod render;
 pub mod shortcut;
+pub mod third_party;
 pub mod timer;
 pub mod update;
 pub mod view_process;
diff --git a/zng-app/src/third_party.rs b/zng-app/src/third_party.rs
new file mode 100644
index 000000000..60f409de7
--- /dev/null
+++ b/zng-app/src/third_party.rs
@@ -0,0 +1,98 @@
+//! Third party licenses service and types.
+
+use zng_app_context::app_local;
+pub use zng_tp_licenses::{License, LicenseUsed, User, UserLicense};
+use zng_var::{var, ArcVar, Var as _};
+
+use crate::{
+    event::{command, CommandNameExt as _},
+    view_process::VIEW_PROCESS,
+};
+
+/// Third party licenses.
+pub struct LICENSES;
+
+impl LICENSES {
+    /// Aggregates all registered third party licenses, grouped by license, sorted by name.
+    ///
+    /// Exact licenses and users are deduplicated.
+    pub fn licenses(&self) -> Vec<LicenseUsed> {
+        let mut r = vec![];
+
+        let sv = LICENSES_SV.read();
+
+        for l in sv.sources.iter() {
+            let l = l();
+            zng_tp_licenses::merge_licenses(&mut r, l);
+        }
+
+        if sv.include_view_process.get() {
+            let l = self.view_process_licenses();
+            zng_tp_licenses::merge_licenses(&mut r, l);
+        }
+
+        zng_tp_licenses::sort_licenses(&mut r);
+
+        r
+    }
+
+    /// Third party licenses provided by the view-process, grouped by license, sorted by name.
+    ///
+    /// Returns an empty vec if there is no view-process running or the view-process does not provide any license.
+    pub fn view_process_licenses(&self) -> Vec<LicenseUsed> {
+        let mut r = VIEW_PROCESS.third_party_licenses().unwrap_or_default();
+        zng_tp_licenses::sort_licenses(&mut r);
+        r
+    }
+
+    /// Aggregates all registered third party licenses, by user, sorted by name.
+    ///
+    /// Exact licenses and users are deduplicated.
+    pub fn user_licenses(&self) -> Vec<UserLicense> {
+        zng_tp_licenses::user_licenses(&self.licenses())
+    }
+
+    /// Third party licenses provided by the view-process, by user, sorted by name.
+    ///
+    /// Returns an empty vec if there is no view-process running or the view-process does not provide any license.
+    pub fn view_process_user_licenses(&self) -> Vec<UserLicense> {
+        zng_tp_licenses::user_licenses(&self.view_process_licenses())
+    }
+
+    /// If view-process provided third party licenses are included in [`licenses`].
+    ///
+    /// Note that prebuilt view-process licenses may not be found by license scraper tools.
+    ///
+    /// This is `true` by default.
+    ///
+    /// [`licenses`]: Self::licenses
+    pub fn include_view_process(&self) -> ArcVar<bool> {
+        LICENSES_SV.read().include_view_process.clone()
+    }
+
+    /// Register a function that loads some third party licenses used by this app.
+    pub fn register(&self, source: fn() -> Vec<LicenseUsed>) {
+        LICENSES_SV.write().sources.push(source);
+    }
+}
+
+app_local! {
+    static LICENSES_SV: Licenses = Licenses { sources: vec![], include_view_process: var(true), };
+}
+
+struct Licenses {
+    sources: Vec<fn() -> Vec<LicenseUsed>>,
+    include_view_process: ArcVar<bool>,
+}
+
+command! {
+    /// Open or focus the third party licenses screen.
+    ///
+    /// Note that the `zng` crate provides a default implementation for this command, you can override this
+    /// default by handling the command in an [`on_pre_event`] handle.
+    ///
+    /// [`on_pre_event`]: crate::event::Command::on_pre_event
+    pub static OPEN_LICENSES_CMD = {
+        name: "Third Party Licenses"
+    };
+}
diff --git a/zng-app/src/view_process.rs b/zng-app/src/view_process.rs
index b73a0cded..50c2dc531 100644
--- a/zng-app/src/view_process.rs
+++ b/zng-app/src/view_process.rs
@@ -252,6 +252,14 @@ impl VIEW_PROCESS {
         }
     }
 
+    /// Licenses that may be required to be displayed in the app about screen.
+    ///
+    /// This is specially important for prebuilt view users, as the tools that scrap licenses
+    /// may not find the prebuilt dependencies.
+    pub fn third_party_licenses(&self) -> Result<Vec<crate::third_party::LicenseUsed>> {
+        self.write().process.third_party_licenses()
+    }
+
     /// Call an extension with custom encoded payload.
     pub fn app_extension_raw(&self, extension_id: ApiExtensionId, extension_request: ApiExtensionPayload) -> Result<ApiExtensionPayload> {
         self.write().process.app_extension(extension_id, extension_request)
diff --git a/zng-tp-licenses/Cargo.toml b/zng-tp-licenses/Cargo.toml
new file mode 100644
index 000000000..1acb34005
--- /dev/null
+++ b/zng-tp-licenses/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "zng-tp-licenses"
+version = "0.2.0"
+edition = "2021"
+license = "Apache-2.0 OR MIT"
+readme = "../zng-app/README.md"
+description = "Part of the zng project."
+documentation = "https://zng-ui.github.io/doc/zng_third_party_licenses"
+repository = "https://github.com/zng-ui/zng"
+categories = ["gui"]
+keywords = ["gui", "ui", "user-interface", "zng"]
+
+[features]
+# Include helpers for collecting third-party licenses.
+build = ["dep:serde_json", "dep:deflate", "dep:bincode"]
+# Include helpers for deserializing `build` encoded licenses.
+bundle = ["dep:inflate", "dep:bincode"]
+
+[dependencies]
+zng-txt = { path = "../zng-txt", version = "0.2.3" }
+
+serde = { version = "1.0", features = ["derive"] }
+
+serde_json = { version = "1.0", optional = true }
+deflate = { version = "*", optional = true }
+inflate = { version = "*", optional = true }
+bincode = { version = "1.3", optional = true }
\ No newline at end of file
diff --git a/zng-tp-licenses/src/lib.rs b/zng-tp-licenses/src/lib.rs
new file mode 100644
index 000000000..1590eb584
--- /dev/null
+++ b/zng-tp-licenses/src/lib.rs
@@ -0,0 +1,280 @@
+#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/master/examples/res/image/zng-logo-icon.png")]
+#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/master/examples/res/image/zng-logo.png")]
+#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
+//! Third party license management and collection.
+//!
+//! Some licenses require that they must be accessible in the final binary, usually in an "about" screen. This
+//! crate defines the data type for this about screen and optionally uses `cargo about` to find and bundle all
+//! license files.
+//!
+//!
+
+use std::fmt;
+
+use serde::{Deserialize, Serialize};
+use zng_txt::Txt;
+
+/// Represents a license and dependencies that use it.
+#[derive(Serialize, Deserialize, Clone)]
+pub struct LicenseUsed {
+    /// License name and text.
+    pub license: License,
+    /// Project or packages that use this license.
+    pub used_by: Vec<User>,
+}
+impl fmt::Debug for LicenseUsed {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("License")
+            .field("license.id", &self.license.id)
+            .field("used_by", &self.used_by)
+            .finish_non_exhaustive()
+    }
+}
+impl LicenseUsed {
+    /// Invert data to be keyed by user.
+    pub fn user_licenses(&self) -> Vec<UserLicense> {
+        self.used_by
+            .iter()
+            .map(|u| UserLicense {
+                user: u.clone(),
+                license: self.license.clone(),
+            })
+            .collect()
+    }
+}
+
+/// Invert data to be keyed by user, also sorts by user name.
+pub fn user_licenses(licenses: &[LicenseUsed]) -> Vec<UserLicense> {
+    let mut r: Vec<_> = licenses.iter().flat_map(|l| l.user_licenses()).collect();
+    r.sort_by(|a, b| a.user.name.cmp(&b.user.name));
+    r
+}
+
+/// Represents a license user with license.
+#[derive(Clone, PartialEq, Eq)]
+pub struct UserLicense {
+    /// License user.
+    pub user: User,
+    /// License used.
+    pub license: License,
+}
+impl fmt::Debug for UserLicense {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("UserLicense")
+            .field("user", &self.user)
+            .field("license.id", &self.license.id)
+            .finish()
+    }
+}
+
+/// Represents a license id, name and text.
+#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, Hash)]
+pub struct License {
+    /// License [SPDX] id.
+    ///
+    /// [SPDX]: https://spdx.org/licenses/
+    pub id: Txt,
+    /// License name.
+    pub name: Txt,
+    /// License text.
+    pub text: Txt,
+}
+
+/// Represents a project or package that uses a license.
+#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug, Clone)]
+pub struct User {
+    /// Project or package name.
+    pub name: Txt,
+    /// Package version.
+    #[serde(default)]
+    pub version: Txt,
+    /// Project or package URL.
+    #[serde(default)]
+    pub url: Txt,
+}
+
+/// Merge `licenses` into `into`.
+///
+/// The licenses and users are not sorted, call [`sort_licenses`] after merging all licenses.
+pub fn merge_licenses(into: &mut Vec<LicenseUsed>, licenses: Vec<LicenseUsed>) {
+    for license in licenses {
+        if let Some(l) = into.iter_mut().find(|l| l.license == license.license) {
+            for user in license.used_by {
+                if !l.used_by.contains(&user) {
+                    l.used_by.push(user);
+                }
+            }
+        } else {
+            into.push(license);
+        }
+    }
+}
+
+/// Sort vec by license name, and users of each license by name.
+pub fn sort_licenses(l: &mut Vec<LicenseUsed>) {
+    l.sort_by(|a, b| a.license.name.cmp(&b.license.name));
+    for l in l {
+        l.used_by.sort_by(|a, b| a.name.cmp(&b.name));
+    }
+}
+
+/// Calls [`cargo about`] for the crate.
+///
+/// This method must be used in build scripts (`build.rs`).
+///
+/// # Panics
+///
+/// Panics for any error, including `cargo about` errors and JSON deserialization errors.
+///
+/// [`cargo about`]: https://github.com/EmbarkStudios/cargo-about
+#[cfg(feature = "build")]
+pub fn collect_cargo_about(about_cfg_path: &str) -> Vec<LicenseUsed> {
+    let mut cargo_about = std::process::Command::new("cargo");
+    cargo_about
+        .arg("about")
+        .arg("generate")
+        .arg("--format")
+        .arg("json")
+        .arg("--all-features");
+
+    if !about_cfg_path.is_empty() {
+        cargo_about.arg("-c").arg(about_cfg_path);
+    }
+
+    let output = cargo_about.output().expect("error calling `cargo about`");
+    let error = String::from_utf8(output.stderr).unwrap();
+    assert!(
+        output.status.success(),
+        "error code calling `cargo about`, {:?}\nstderr:\n{error}",
+        output.status
+    );
+
+    let json = String::from_utf8(output.stdout).unwrap();
+
+    parse_cargo_about(&json).expect("error parsing `cargo about` output")
+}
+
+/// Parse the output of [`cargo about`].
+///
+/// Example command:
+///
+/// ```console
+/// cargo about generate -c .cargo/about.toml --format json --workspace --all-features
+/// ```
+///
+/// See also [`collect_cargo_about`] that calls the command.
+///
+/// [`cargo about`]: https://github.com/EmbarkStudios/cargo-about
+#[cfg(feature = "build")]
+pub fn parse_cargo_about(json: &str) -> Result<Vec<LicenseUsed>, serde_json::Error> {
+    #[derive(Deserialize)]
+    struct Output {
+        licenses: Vec<LicenseJson>,
+    }
+    #[derive(Deserialize)]
+    struct LicenseJson {
+        id: Txt,
+        name: Txt,
+        text: Txt,
+        used_by: Vec<UsedBy>,
+    }
+    impl From<LicenseJson> for LicenseUsed {
+        fn from(value: LicenseJson) -> Self {
+            Self {
+                license: License {
+                    id: value.id,
+                    name: value.name,
+                    text: value.text,
+                },
+                used_by: value.used_by.into_iter().map(Into::into).collect(),
+            }
+        }
+    }
+    #[derive(Deserialize)]
+    struct UsedBy {
+        #[serde(rename = "crate")]
+        crate_: Crate,
+    }
+    #[derive(Deserialize)]
+    struct Crate {
+        name: Txt,
+        version: Txt,
+        #[serde(default)]
+        repository: Option<Txt>,
+    }
+    impl From<UsedBy> for User {
+        fn from(value: UsedBy) -> Self {
+            let repo = value.crate_.repository.unwrap_or_default();
+            Self {
+                version: value.crate_.version,
+                url: if repo.is_empty() {
+                    zng_txt::formatx!("https://crates.io/crates/{}", value.crate_.name)
+                } else {
+                    repo
+                },
+                name: value.crate_.name,
+            }
+        }
+    }
+
+    serde_json::from_str::<Output>(json).map(|o| o.licenses.into_iter().map(Into::into).collect())
+}
+
+/// Bincode serialize and deflate the licenses.
+///
+/// # Panics
+///
+/// Panics in case of any error.
+#[cfg(feature = "build")]
+pub fn encode_licenses(licenses: &[LicenseUsed]) -> Vec<u8> {
+    deflate::deflate_bytes(&bincode::serialize(licenses).expect("bincode error"))
+}
+
+/// Encode licenses and write to the output file that is included by [`include_bundle!`].
+///
+/// # Panics
+///
+/// Panics in case of any error.
+#[cfg(feature = "build")]
+pub fn write_bundle(licenses: &[LicenseUsed]) {
+    let bin = encode_licenses(licenses);
+    std::fs::write(format!("{}/zng-tp-licenses.bin", std::env::var("OUT_DIR").unwrap()), bin).expect("error writing file");
+}
+
+/// Includes the bundle file generated using [`write_bundle`].
+///
+/// This macro output is a `Vec<License>`. Note that if not built with `feature = "bundle"` this
+/// macro always returns an empty vec.
+#[macro_export]
+#[cfg(feature = "bundle")]
+macro_rules! include_bundle {
+    () => {
+        $crate::include_bundle!(concat!(env!("OUT_DIR"), "/zng-tp-licenses.bin"))
+    };
+    ($custom_name:expr) => {{
+        $crate::decode_licenses(include_bytes!($custom_name))
+    }};
+}
+
+/// Includes the bundle file generated using [`write_bundle`].
+///
+/// This macro output is a `Vec<License>`. Note that if not built with `feature = "bundle"` this
+/// macro always returns an empty vec.
+#[macro_export]
+#[cfg(not(feature = "bundle"))]
+macro_rules! include_bundle {
+    () => {
+        $crate::include_bundle!(concat!(env!("OUT_DIR"), "/zng-tp-licenses.bin"))
+    };
+    ($custom_name:expr) => {{
+        Vec::<$crate::LicenseUsed>::new()
+    }};
+}
+
+/// Decode licenses encoded with [`encode_licenses`]. Note that the encoded format is only guaranteed to work
+/// if both encoding and decoding is made with the same `Cargo.lock` dependencies.
+#[cfg(feature = "bundle")]
+pub fn decode_licenses(bin: &[u8]) -> Vec<LicenseUsed> {
+    let bin = inflate::inflate_bytes(bin).expect("invalid bundle deflate binary");
+    bincode::deserialize(&bin).expect("invalid bundle bincode binary")
+}
diff --git a/zng-txt/src/lib.rs b/zng-txt/src/lib.rs
index b7f3b6d48..9a08d4e66 100644
--- a/zng-txt/src/lib.rs
+++ b/zng-txt/src/lib.rs
@@ -605,6 +605,16 @@ impl std::fmt::Write for Txt {
         Ok(())
     }
 }
+impl PartialOrd for Txt {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+impl Ord for Txt {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.as_str().cmp(other.as_str())
+    }
+}
 
 /// A trait for converting a value to a [`Txt`].
 ///
diff --git a/zng-view-api/Cargo.toml b/zng-view-api/Cargo.toml
index 9e738da7e..47aff643d 100644
--- a/zng-view-api/Cargo.toml
+++ b/zng-view-api/Cargo.toml
@@ -20,6 +20,7 @@ default = ["ipc"]
 # minor cost, something like a 1ms per 3MB frame request.
 ipc = ["ipc-channel", "duct"]
 
+# Implement `IntoVar<T>` for API types.
 var = ["zng-var"]
 
 [dependencies]
@@ -30,6 +31,7 @@ euclid = { version = "0.22.6", features = [
 
 zng-unit = { path = "../zng-unit", version = "0.2.3" }
 zng-txt = { path = "../zng-txt", version = "0.2.3" }
+zng-tp-licenses = { path = "../zng-tp-licenses", version = "0.2.0" }
 zng-var = { path = "../zng-var", version = "0.2.4", optional = true }
 
 serde = { version = "1.0", features = ["derive"] }
@@ -52,4 +54,4 @@ serde_variant = "0.1"
 unic-langid = { version = "0.9", features = ["serde"] }
 
 [package.metadata.docs.rs]
-all-features = true
\ No newline at end of file
+all-features = true
diff --git a/zng-view-api/src/lib.rs b/zng-view-api/src/lib.rs
index 5478fd585..58886a198 100644
--- a/zng-view-api/src/lib.rs
+++ b/zng-view-api/src/lib.rs
@@ -487,6 +487,12 @@ declare_api! {
     /// Enable or disable IME by setting a cursor area.
     pub fn set_ime_area(&mut self, id: WindowId, area: Option<DipRect>);
 
+    /// Licenses that may be required to be displayed in the app about screen.
+    ///
+    /// This is specially important for prebuilt view users, as the tools that scrap licenses
+    /// may not find the prebuilt dependencies.
+    pub fn third_party_licenses(&mut self) -> Vec<zng_tp_licenses::LicenseUsed>;
+
     /// Call the API extension.
     ///
     /// The `extension_id` is the index of an extension in the extensions list provided by the view-process on init.
diff --git a/zng-view/Cargo.toml b/zng-view/Cargo.toml
index bfff53d4a..05b64ef79 100644
--- a/zng-view/Cargo.toml
+++ b/zng-view/Cargo.toml
@@ -29,6 +29,13 @@ ipc = ["zng-view-api/ipc"]
 # Feature enabled by default.
 software = ["dep:swgl"]
 
+# Bundle third party licenses.
+#
+# Needs `cargo-about` and Internet connection during build.
+#
+# Not enabled by default. Note that `"view_prebuilt"` always bundles licenses.
+bundle_licenses = ["zng-tp-licenses/bundle"]
+
 # Enable AVIF decoder and encoder.
 #
 # Note that this enables the `dav1d` dependency that is not trivial to build.
@@ -46,6 +53,7 @@ swgl = { git = "https://github.com/zng-ui/zng-webrender.git", package = "zng-swg
 zng-view-api = { path = "../zng-view-api", version = "0.2.4", default-features = false }
 zng-unit = { path = "../zng-unit", version = "0.2.3" }
 zng-txt = { path = "../zng-txt", version = "0.2.3" }
+zng-tp-licenses = { path = "../zng-tp-licenses", version = "0.2.0" }
 
 tracing = "0.1"
 gleam = "0.15.0" # matches webrender
@@ -120,6 +128,9 @@ tempfile = "3.10"
 
 # prebuild profile defined in the workspace toml
 
+[build-dependencies]
+zng-tp-licenses = { path = "../zng-tp-licenses", version = "0.2.0", features = ["build"] }
+
 [package.metadata.docs.rs]
 all-features = true
 targets = ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc", "x86_64-apple-darwin"]
diff --git a/zng-view/build.rs b/zng-view/build.rs
new file mode 100644
index 000000000..96a85d0da
--- /dev/null
+++ b/zng-view/build.rs
@@ -0,0 +1,58 @@
+fn main() {
+    #[cfg(feature = "bundle_licenses")]
+    tp_licenses();
+}
+
+#[cfg(feature = "bundle_licenses")]
+fn tp_licenses() {
+    #[allow(unused_mut)]
+    let mut licenses = zng_tp_licenses::collect_cargo_about("../.cargo/about.toml");
+    #[cfg(feature = "avif")]
+    avif_licenses(&mut licenses);
+    zng_tp_licenses::write_bundle(&licenses);
+}
+
+#[cfg(feature = "avif")]
+fn avif_licenses(l: &mut Vec<zng_tp_licenses::LicenseUsed>) {
+    use zng_tp_licenses::*;
+
+    l.push(LicenseUsed {
+        license: License {
+            id: "BSD-2-Clause".into(),
+            name: r#"BSD 2-Clause "Simplified" License"#.into(),
+            text: DAV1D_COPYING.into(),
+        },
+        used_by: vec![User {
+            name: "dav1d".into(),
+            version: "1.3.0".into(), // from .github/workflows/release.yml
+            url: "https://code.videolan.org/videolan/dav1d".into(),
+        }],
+    });
+}
+#[cfg(feature = "avif")]
+const DAV1D_COPYING: &str = r##"
+Copyright © 2018-2019, VideoLAN and dav1d authors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"##;
diff --git a/zng-view/src/lib.rs b/zng-view/src/lib.rs
index 1b7710860..e60237e7d 100644
--- a/zng-view/src/lib.rs
+++ b/zng-view/src/lib.rs
@@ -1891,6 +1891,10 @@ impl Api for App {
         }
     }
 
+    fn third_party_licenses(&mut self) -> Vec<zng_tp_licenses::LicenseUsed> {
+        zng_tp_licenses::include_bundle!()
+    }
+
     fn app_extension(&mut self, extension_id: ApiExtensionId, extension_request: ApiExtensionPayload) -> ApiExtensionPayload {
         self.exts.call_command(extension_id, extension_request)
     }
diff --git a/zng/Cargo.toml b/zng/Cargo.toml
index 757fb37d8..144c77631 100644
--- a/zng/Cargo.toml
+++ b/zng/Cargo.toml
@@ -155,6 +155,12 @@ view = ["dep:zng-view"]
 #
 # Feature enabled by default.
 view_software = ["zng-view?/software"]
+# Collects and bundles third-party licenses used by the `zng-view` crate.
+#
+# Needs `cargo-about` and Internet connection during build.
+#
+# Not enabled by default. Note that `"view_prebuilt"` always bundles licenses.
+view_bundle_licenses = ["zng-view?/bundle_licenses"]
 
 # Include the default view-process implementation as an embedded precompiled binary.
 view_prebuilt = ["dep:zng-view-prebuilt", "ipc"]
diff --git a/zng/src/lib.rs b/zng/src/lib.rs
index a5c7451c4..4b7e2f71b 100644
--- a/zng/src/lib.rs
+++ b/zng/src/lib.rs
@@ -494,6 +494,7 @@ pub mod style;
 pub mod task;
 pub mod text;
 pub mod text_input;
+pub mod third_party;
 pub mod timer;
 pub mod tip;
 pub mod toggle;
@@ -867,6 +868,8 @@ mod defaults {
                 })
             });
 
+            crate::third_party::setup_default_view();
+
             #[cfg(feature = "material_icons_outlined")]
             {
                 use zng_ext_clipboard::*;
diff --git a/zng/src/third_party.rs b/zng/src/third_party.rs
new file mode 100644
index 000000000..6abb74d2a
--- /dev/null
+++ b/zng/src/third_party.rs
@@ -0,0 +1,271 @@
+//! Third party licenses service and types.
+//!
+//! Rust projects depend on many crated with a variety of licenses, some of these licenses require that they must be
+//! displayed in the app binary, usually in an "about" screen. This module can be used together with the [`zng_tp_licenses`]
+//! crate to collect and bundle licenses of all used crates in your project.
+//!
+//! The [`LICENSES`] service serves as an aggregation center for licenses of multiple sources, the [`OPEN_LICENSES_CMD`]
+//! can be implemented [`on_pre_event`] to show a custom licenses screen, or it can just be used to show the default
+//! screen provided by the default app.
+//!
+//! # Bundle Setup
+//!
+//! Follow these steps to configure your crate and build workflow to collect and bundle crate licenses.
+//!
+//! ### Install `cargo about`
+//!
+//! To collect and bundle licenses in your project you must have [`cargo-about`] installed:
+//!
+//! ```console
+//! cargo install cargo-about
+//! ```
+//!
+//! Next add file `.cargo/about.toml` in your crate or workspace root:
+//!
+//! ```toml
+//! # cargo about generate -c .cargo/about.toml --format json --workspace --all-features
+//!
+//! accepted = [
+//!     "Apache-2.0",
+//!     "MIT",
+//!     "MPL-2.0",
+//!     "Unicode-DFS-2016",
+//!     "BSL-1.0",
+//!     "BSD-2-Clause",
+//!     "BSD-3-Clause",
+//!     "ISC",
+//!     "Zlib",
+//!     "CC0-1.0",
+//! ]
+//!
+//! ignore-build-dependencies = true
+//! ignore-dev-dependencies = true
+//! filter-noassertion = true
+//! private = { ignore = true }
+//! ```
+//!
+//! Next call the command to test and modify the `accepted` config:
+//!
+//! ```console
+//! cargo about generate -c .cargo/about.toml --format json --workspace --all-features
+//! ```
+//!
+//! If the command prints a massive JSON dump, you are done with this step.
+//!
+//! ### Add `zng-tp-licenses`
+//!
+//! Next, add dependency to the [`zng_tp_licenses`] your crate `Cargo.toml`:
+//!
+//! ```toml
+//! [package]
+//! resolver = "2" # recommended, to not include "build" feature in the normal dependency.
+//!
+//! [features]
+//! # Recommended, so you only bundle in release builds.
+//! #
+//! # Note that if you use a feature, don't forget to build with `--features bundle_licenses`.
+//! bundle_licenses = ["dep:zng-tp-licenses"]
+//!
+//! [dependencies]
+//! zng-tp-licenses = { version = "0.2.0", feature = ["bundle"], optional = true }
+//!
+//! [build-dependencies]
+//! zng-tp-licenses = { version = "0.2.0", feature = ["build"], optional = true }
+//! ```
+//!
+//! ### Implement Bundle
+//!
+//! Next, in your crates build script (`build.rs`) add:
+//!
+//! ```
+//! fn main() {
+//!     #[cfg(feature = "bundle_licenses")]
+//!     {
+//!         let licenses = zng_tp_licenses::collect_cargo_about("../.cargo/about.toml");
+//!         zng_tp_licenses::write_bundle(&licenses);
+//!     }
+//! }
+//! ```
+//!
+//! Implement a function that includes the bundle and decodes it. Register the function it in your app init code:
+//!
+//! ```
+//! #[cfg(feature = "bundle_licenses")]
+//! fn bundled_licenses() -> Vec<zng::third_party::LicenseUsed> {
+//!     zng_tp_licenses::include_bundle!()
+//! }
+//!
+//! # fn demo() {
+//! # use zng::prelude::*;
+//! APP.defaults().run(async {
+//!     #[cfg(feature = "bundle_licenses")]
+//!     zng::third_party::LICENSES.register(bundled_licenses);
+//! });
+//! # }
+//! # fn main() { }
+//! ```
+//!
+//! ### Review Licenses
+//!
+//! Call the [`OPEN_LICENSES_CMD`] in a test button, check if all the required licenses are present,
+//! `cargo about` and `zng_tp_licenses` are a **best effort only** helpers, you must ensure that the generated results
+//! meet yours or your company's legal obligations.
+//!
+//! ```
+//! use zng::prelude::*;
+//!
+//! fn review_licenses() -> impl UiNode {
+//!     // zng::third_party::LICENSES.include_view_process().set(false);
+//!
+//!     Button!(zng::third_party::OPEN_LICENSES_CMD)
+//! }
+//! ```
+//!
+//! #### Limitations
+//!
+//! Only crate licenses reachable thought cargo metadata are included. Static linked libraries in `-sys` crates may
+//! have required licenses that are not included. Other bundled resources such as fonts and images may also be licensed.
+//!
+//! The [`LICENSES`] service accepts multiple sources, so you can implement your own custom bundle, the [`zng_tp_licenses`]
+//! provides helpers for manually encoding (compressing) licenses. See the `zng-view` build script for an example of how
+//! to include more licenses.
+//!
+//! # Full API
+//!
+//! See [`zng_app::third_party`] and [`zng_tp_licenses`] for the full API.
+//!
+//! [`zng_tp_licenses`]: https://zng-ui.github.io/doc/zng_tp_licenses/
+//! [`cargo-about`]: https://github.com/EmbarkStudios/cargo-about/
+//! [`on_pre_event`]: crate::event::Command::on_pre_event
+
+pub use zng_app::third_party::{License, LicenseUsed, User, UserLicense, LICENSES, OPEN_LICENSES_CMD};
+
+use crate::prelude::*;
+
+pub(crate) fn setup_default_view() {
+    let mut id = None;
+    OPEN_LICENSES_CMD
+        .on_event(
+            true,
+            app_hn!(|args: &zng_app::event::AppCommandArgs, _| {
+                if args.propagation().is_stopped() {
+                    return;
+                }
+                args.propagation().stop();
+
+                let parent = WINDOWS.focused_window_id();
+
+                let id = *id.get_or_insert_with(WindowId::new_unique);
+                WINDOWS.focus_or_open(id, async move {
+                    if let Some(p) = parent {
+                        if let Ok(p) = WINDOWS.vars(p) {
+                            let v = WINDOW.vars();
+                            p.icon().set_bind(&v.icon()).perm();
+                        }
+                    }
+
+                    Window! {
+                        title = "Third Party Licenses";
+                        child = default_view();
+                        parent;
+                    }
+                });
+            }),
+        )
+        .perm();
+}
+
+fn default_view() -> impl UiNode {
+    let licenses = LICENSES.user_licenses();
+    let selected = var(licenses.first().cloned().unwrap_or_else(|| UserLicense {
+        user: User {
+            name: "<none>".into(),
+            version: "".into(),
+            url: "".into(),
+        },
+        license: License {
+            id: "<none>".into(),
+            name: "No license data".into(),
+            text: "".into(),
+        },
+    }));
+    let search = var(Txt::from(""));
+
+    Container! {
+        child_start = Stack! {
+            direction = StackDirection::top_to_bottom();
+            children = ui_vec![
+                // search
+                TextInput! {
+                    txt = search.clone();
+                    widget::background = Text! {
+                        txt = "search";
+                        layout::padding = (8, 16, 8, 27); // +1 border width
+                        color::filter::opacity = 50.pct();
+                        widget::visibility = search.map(|t| t.is_empty().into());
+                    };
+                },
+                // list
+                Scroll! {
+                    mode = zng::scroll::ScrollMode::VERTICAL;
+                    child_align = Align::FILL;
+                    child = DataView! {
+                        layout::sticky_width = true;
+                        view::<Txt> = search, hn!(selected, |a: &DataViewArgs<Txt>| {
+                            let search = a.data().get();
+                            let licenses = if search.is_empty() {
+                                licenses.clone()
+                            } else {
+                                licenses.iter().filter(|t| t.user.name.contains(search.as_str())).cloned().collect()
+                            };
+
+                            a.set_view(Stack! {
+                                toggle::selector = toggle::Selector::single(selected.clone());
+                                direction = StackDirection::top_to_bottom();
+                                children = licenses.into_iter().map(default_item_view).collect::<UiNodeVec>();
+                            })
+                        });
+                    };
+                }
+            ];
+        }, 0;
+        // selected
+        child = Scroll! {
+            mode = zng::scroll::ScrollMode::VERTICAL;
+            child_align = Align::TOP_START;
+            padding = 10;
+            child = zng::markdown::Markdown!(selected.map(default_markdown));
+        };
+    }
+}
+
+fn default_item_view(item: UserLicense) -> impl UiNode {
+    Toggle! {
+        child = Text!(item.user.name.clone());
+        value = item;
+        child_align = layout::Align::START;
+        widget::corner_radius = 0;
+        layout::padding = 2;
+        widget::border = unset!;
+    }
+}
+
+fn default_markdown(item: &UserLicense) -> Txt {
+    use std::fmt::*;
+
+    let mut t = Txt::from("");
+
+    if item.user.version.is_empty() {
+        writeln!(&mut t, "# {}\n", item.user.name).unwrap();
+    } else {
+        writeln!(&mut t, "# {} - {}\n", item.user.name, item.user.version).unwrap();
+    }
+    if !item.user.url.is_empty() {
+        writeln!(&mut t, "[{0}]({0})\n", item.user.url).unwrap();
+    }
+
+    writeln!(&mut t, "## {}\n\n```\n{}\n```\n", item.license.name, item.license.text).unwrap();
+
+    t.end_mut();
+    t
+}


Test patch: