From c2b5192fd064637f13242e59c967da445e52938b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 18 Apr 2024 10:10:36 +0200 Subject: [PATCH 01/19] CI: Add link checker --- .github/workflows/links.yml | 29 +++++++++++++ lychee.toml | 82 +++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 .github/workflows/links.yml create mode 100644 lychee.toml diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml new file mode 100644 index 0000000..8bb984c --- /dev/null +++ b/.github/workflows/links.yml @@ -0,0 +1,29 @@ +# Copied from https://github.com/rerun-io/rerun_template +on: [push, pull_request] + +name: Link checker + +jobs: + link-checker: + name: Check links + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Restore link checker cache + uses: actions/cache@v3 + with: + path: .lycheecache + key: cache-lychee-${{ github.sha }} + restore-keys: cache-lychee- + + # Check https://github.com/lycheeverse/lychee on how to run locally. + - name: Link Checker + id: lychee + uses: lycheeverse/lychee-action@v1.9.0 + with: + fail: true + lycheeVersion: "0.14.3" + # When given a directory, lychee checks only markdown, html and text files, everything else we have to glob in manually. + args: | + --base . --cache --max-cache-age 1d . "**/*.rs" "**/*.toml" "**/*.hpp" "**/*.cpp" "**/CMakeLists.txt" "**/*.py" "**/*.yml" diff --git a/lychee.toml b/lychee.toml new file mode 100644 index 0000000..212d70e --- /dev/null +++ b/lychee.toml @@ -0,0 +1,82 @@ +# Copied from https://github.com/rerun-io/rerun_template + +################################################################################ +# Config for the link checker lychee. +# +# Download & learn more at: +# https://github.com/lycheeverse/lychee +# +# Example config: +# https://github.com/lycheeverse/lychee/blob/master/lychee.example.toml +# +# Run `lychee . --dump` to list all found links that are being checked. +# +# Note that by default lychee will only check markdown and html files, +# to check any other files you have to point to them explicitly, e.g.: +# `lychee **/*.rs` +# To make things worse, `exclude_path` is ignored for these globs, +# so local runs with lots of gitignored files will be slow. +# (https://github.com/lycheeverse/lychee/issues/1405) +# +# This unfortunately doesn't list anything for non-glob checks. +################################################################################ + +# Maximum number of concurrent link checks. +# Workaround for "too many open files" error on MacOS, see https://github.com/lycheeverse/lychee/issues/1248 +max_concurrency = 32 + +# Check links inside `` and `
` blocks as well as Markdown code blocks.
+include_verbatim = true
+
+# Proceed for server connections considered insecure (invalid TLS).
+insecure = true
+
+# Exclude these filesystem paths from getting checked.
+exclude_path = [
+  # Unfortunately lychee doesn't yet read .gitignore https://github.com/lycheeverse/lychee/issues/1331
+  # The following entries are there because of that:
+  ".git",
+  "__pycache__",
+  "_deps/",
+  ".pixi",
+  "build",
+  "target_ra",
+  "target_wasm",
+  "target",
+  "venv",
+]
+
+# Exclude URLs and mail addresses from checking (supports regex).
+exclude = [
+  # Skip speculative links
+  '.*?speculative-link',
+
+  # Strings with replacements.
+  '/__VIEWER_VERSION__/', # Replacement variable __VIEWER_VERSION__.
+  '/\$',                  # Replacement variable $.
+  '/GIT_HASH/',           # Replacement variable GIT_HASH.
+  '\{\}',                 # Ignore links with string interpolation.
+  '\$relpath\^',          # Relative paths as used by rerun_cpp's doc header.
+  '%7B.+%7D',             # Ignore strings that look like ready to use links but contain a replacement strings. The URL escaping is for '{.+}' (this seems to be needed for html embedded urls since lychee assumes they use this encoding).
+  '%7B%7D',               # Ignore links with string interpolation, escaped variant.
+
+  # Local links that require further setup.
+  'http://127.0.0.1',
+  'http://localhost',
+  'recording:/',      # rrd recording link.
+  'ws:/',
+  're_viewer.js',     # Build artifact that html is linking to.
+
+  # Api endpoints.
+  'https://fonts.googleapis.com/', # Font API entrypoint, not a link.
+  'https://fonts.gstatic.com/',    # Font API entrypoint, not a link.
+  'https://tel.rerun.io/',         # Analytics endpoint.
+
+  # Avoid rate limiting.
+  'https://crates.io/crates/.*',                  # Avoid crates.io rate-limiting
+  'https://github.com/rerun-io/rerun/commit/\.*', # Ignore links to our own commits (typically in changelog).
+  'https://github.com/rerun-io/rerun/pull/\.*',   # Ignore links to our own pull requests (typically in changelog).
+
+  # Used in rerun_template repo until the user search-replaces `new_repo_name`
+  'https://github.com/rerun-io/new_repo_name',
+]

From a7e94c4975d0176ce072f7ec6cc34b82da5a2142 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:18:23 +0200
Subject: [PATCH 02/19] Improve .gitignore

---
 .gitignore | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index ea8c4bf..747604c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,10 @@
-/target
+# Mac stuff:
+.DS_Store
+
+# Rust compile target directories:
+target
+target_ra
+target_wasm
+
+# https://github.com/lycheeverse/lychee
+.lycheecache

From 8788629b9198bbdb3849f54b8a251d738b6bdfe8 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:23:57 +0200
Subject: [PATCH 03/19] CI: check for typos

---
 .github/workflows/typos.yml | 19 +++++++++++++++++++
 .typos.toml                 |  3 ++-
 2 files changed, 21 insertions(+), 1 deletion(-)
 create mode 100644 .github/workflows/typos.yml

diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml
new file mode 100644
index 0000000..3055f87
--- /dev/null
+++ b/.github/workflows/typos.yml
@@ -0,0 +1,19 @@
+# Copied from https://github.com/rerun-io/rerun_template
+
+# https://github.com/crate-ci/typos
+# Add exceptions to `.typos.toml`
+# install and run locally: cargo install typos-cli && typos
+
+name: Spell Check
+on: [pull_request]
+
+jobs:
+  run:
+    name: Spell Check
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Actions Repository
+        uses: actions/checkout@v4
+
+      - name: Check spelling of entire workspace
+        uses: crate-ci/typos@master
diff --git a/.typos.toml b/.typos.toml
index 939a942..fbea054 100644
--- a/.typos.toml
+++ b/.typos.toml
@@ -3,7 +3,8 @@
 # run:      typos
 
 [files]
-extend-exclude = [".typos.toml", "docs/example_app.js"]
+extend-exclude = ["docs/example_app.js"]
 
 
 [default.extend-words]
+teh = "teh" # part of @teh-cmc

From dafdf5c89333faa70f524fe47ad12334c1257bb4 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:24:14 +0200
Subject: [PATCH 04/19] Add bacon.toml

---
 bacon.toml | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)
 create mode 100644 bacon.toml

diff --git a/bacon.toml b/bacon.toml
new file mode 100644
index 0000000..0476b3e
--- /dev/null
+++ b/bacon.toml
@@ -0,0 +1,70 @@
+# This is a configuration file for the bacon tool
+# More info at https://github.com/Canop/bacon
+
+default_job = "cranky"
+
+[jobs]
+
+[jobs.cranky]
+command = [
+  "cargo",
+  "cranky",
+  "--all-targets",
+  "--all-features",
+  "--color=always",
+]
+need_stdout = false
+watch = ["tests", "benches", "examples"]
+
+[jobs.check]
+command = [
+  "cargo",
+  "check",
+  "--all-targets",
+  "--all-features",
+  "--color=always",
+]
+need_stdout = false
+watch = ["tests", "benches", "examples"]
+
+[jobs.test]
+command = ["cargo", "test", "--color=always"]
+need_stdout = true
+watch = ["tests"]
+
+[jobs.doc]
+command = ["cargo", "doc", "--color=always", "--all-features", "--no-deps"]
+need_stdout = false
+
+# if the doc compiles, then it opens in your browser and bacon switches
+# to the previous job
+[jobs.doc-open]
+command = [
+  "cargo",
+  "doc",
+  "--color=always",
+  "--all-features",
+  "--no-deps",
+  "--open",
+]
+need_stdout = false
+on_success = "back" # so that we don't open the browser at each change
+
+# You can run your application and have the result displayed in bacon,
+# *if* it makes sense for this crate. You can run an example the same
+# way. Don't forget the `--color always` part or the errors won't be
+# properly parsed.
+[jobs.run]
+command = ["cargo", "run", "--color=always"]
+need_stdout = true
+
+# You may define here keybindings that would be specific to
+# a project, for example a shortcut to launch a specific job.
+# Shortcuts to internal functions (scrolling, toggling, etc.)
+# should go in your personal prefs.toml file instead.
+[keybindings]
+i = "job:initial"
+c = "job:cranky"
+d = "job:doc-open"
+t = "job:test"
+r = "job:run"

From e432174ca900c6c5fe4e290f1f5903645ef1cca9 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:26:38 +0200
Subject: [PATCH 05/19] Update crate `mio`

---
 Cargo.lock | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 1330281..66664e0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1931,9 +1931,9 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "0.8.10"
+version = "0.8.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
 dependencies = [
  "libc",
  "wasi",

From 16c85acc73582d2b7ec4001ab2527b0a643a1e47 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:29:06 +0200
Subject: [PATCH 06/19] Update deny.toml

---
 deny.toml | 96 +++++++++++++++++++++++++++++--------------------------
 1 file changed, 51 insertions(+), 45 deletions(-)

diff --git a/deny.toml b/deny.toml
index c9148fe..ea8767f 100644
--- a/deny.toml
+++ b/deny.toml
@@ -1,3 +1,5 @@
+# Copied from https://github.com/rerun-io/rerun_template
+#
 # https://github.com/EmbarkStudios/cargo-deny
 #
 # cargo-deny checks our dependency tree for copy-left licenses,
@@ -6,77 +8,81 @@
 # Install: `cargo install cargo-deny`
 # Check: `cargo deny check`.
 
+
 # Note: running just `cargo deny check` without a `--target` can result in
 # false positives due to https://github.com/EmbarkStudios/cargo-deny/issues/324
+[graph]
 targets = [
-    { triple = "aarch64-apple-darwin" },
-    { triple = "i686-pc-windows-gnu" },
-    { triple = "i686-pc-windows-msvc" },
-    { triple = "i686-unknown-linux-gnu" },
-    { triple = "wasm32-unknown-unknown" },
-    { triple = "x86_64-apple-darwin" },
-    { triple = "x86_64-pc-windows-gnu" },
-    { triple = "x86_64-pc-windows-msvc" },
-    { triple = "x86_64-unknown-linux-gnu" },
-    { triple = "x86_64-unknown-linux-musl" },
-    { triple = "x86_64-unknown-redox" },
+  { triple = "aarch64-apple-darwin" },
+  { triple = "i686-pc-windows-gnu" },
+  { triple = "i686-pc-windows-msvc" },
+  { triple = "i686-unknown-linux-gnu" },
+  { triple = "wasm32-unknown-unknown" },
+  { triple = "x86_64-apple-darwin" },
+  { triple = "x86_64-pc-windows-gnu" },
+  { triple = "x86_64-pc-windows-msvc" },
+  { triple = "x86_64-unknown-linux-gnu" },
+  { triple = "x86_64-unknown-linux-musl" },
+  { triple = "x86_64-unknown-redox" },
 ]
+all-features = true
+
 
 [advisories]
-vulnerability = "deny"
-unmaintained = "warn"
-yanked = "deny"
-ignore = [
-    "RUSTSEC-2021-0019", # https://rustsec.org/advisories/RUSTSEC-2021-0019 - Multiple soundness issues. From xcb <- x11-clipboard <- copypasta <- egui-winit
-]
+version = 2
+ignore = []
+
 
 [bans]
 multiple-versions = "deny"
-wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed
+wildcards = "allow" # We use them for examples
 deny = [
-    { name = "openssl" },     # prefer rustls
-    { name = "openssl-sys" }, # prefer rustls
+  { name = "openssl", reason = "Use rustls" },
+  { name = "openssl-sys", reason = "Use rustls" },
 ]
-
 skip = []
 skip-tree = [
-    { name = "eframe" }, # Only used by example app
+  { name = "eframe" }, # Only used by example app
 ]
 
 
 [licenses]
-unlicensed = "deny"
-allow-osi-fsf-free = "neither"
-confidence-threshold = 0.92 # We want really high confidence when inferring licenses from text
-copyleft = "deny"
+version = 2
+private = { ignore = true }
+confidence-threshold = 0.93 # We want really high confidence when inferring licenses from text
 allow = [
-    "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html
-    "Apache-2.0",                     # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)
-    "BSD-2-Clause",                   # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd)
-    "BSD-3-Clause",                   # https://tldrlegal.com/license/bsd-3-clause-license-(revised)
-    "BSL-1.0",                        # https://tldrlegal.com/license/boost-software-license-1.0-explained
-    "CC0-1.0",                        # https://creativecommons.org/publicdomain/zero/1.0/
-    "ISC",                            # https://tldrlegal.com/license/-isc-license
-    "LicenseRef-UFL-1.0",             # https://tldrlegal.com/license/ubuntu-font-license,-1.0 - no official SPDX, see https://github.com/emilk/egui/issues/2321
-    "MIT",                            # https://tldrlegal.com/license/mit-license
-    "MPL-2.0",                        # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11
-    "OFL-1.1",                        # https://spdx.org/licenses/OFL-1.1.html
-    "OpenSSL",                        # https://www.openssl.org/source/license.html
-    "Unicode-DFS-2016",               # https://spdx.org/licenses/Unicode-DFS-2016.html
-    "Zlib",                           # https://tldrlegal.com/license/zlib-libpng-license-(zlib)
+  "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html
+  "Apache-2.0",                     # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)
+  "BSD-2-Clause",                   # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd)
+  "BSD-3-Clause",                   # https://tldrlegal.com/license/bsd-3-clause-license-(revised)
+  "BSL-1.0",                        # https://tldrlegal.com/license/boost-software-license-1.0-explained
+  "CC0-1.0",                        # https://creativecommons.org/publicdomain/zero/1.0/
+  "ISC",                            # https://www.tldrlegal.com/license/isc-license
+  "LicenseRef-UFL-1.0",             # See https://github.com/emilk/egui/issues/2321
+  "MIT-0",                          # https://choosealicense.com/licenses/mit-0/
+  "MIT",                            # https://tldrlegal.com/license/mit-license
+  "MPL-2.0",                        # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux.
+  "OFL-1.1",                        # https://spdx.org/licenses/OFL-1.1.html
+  "OpenSSL",                        # https://www.openssl.org/source/license.html - used on Linux
+  "Unicode-DFS-2016",               # https://spdx.org/licenses/Unicode-DFS-2016.html
+  "Zlib",                           # https://tldrlegal.com/license/zlib-libpng-license-(zlib)
 ]
+exceptions = []
 
 [[licenses.clarify]]
 name = "webpki"
 expression = "ISC"
 license-files = [{ path = "LICENSE", hash = 0x001c7e6c }]
 
-[[licenses.clarify]]
-name = "rustls-webpki"
-expression = "ISC"
-license-files = [{ path = "LICENSE", hash = 0x001c7e6c }]
-
 [[licenses.clarify]]
 name = "ring"
 expression = "MIT AND ISC AND OpenSSL"
 license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]
+
+
+[sources]
+unknown-registry = "deny"
+unknown-git = "deny"
+
+[sources.allow-org]
+github = ["emilk", "rerun-io"]

From 5c79a63174d4123139ee978865d75248827f860c Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:29:57 +0200
Subject: [PATCH 07/19] Small formatting changes

---
 CODE_OF_CONDUCT.md | 1 -
 LICENSE-MIT        | 2 +-
 rust-toolchain     | 4 ++--
 3 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 2b710de..1fbf07c 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -130,4 +130,3 @@ For answers to common questions about this code of conduct, see the FAQ at
 [Mozilla CoC]: https://github.com/mozilla/diversity
 [FAQ]: https://www.contributor-covenant.org/faq
 [translations]: https://www.contributor-covenant.org/translations
-
diff --git a/LICENSE-MIT b/LICENSE-MIT
index 673ea5f..3f6d1ed 100644
--- a/LICENSE-MIT
+++ b/LICENSE-MIT
@@ -1,4 +1,4 @@
-Copyright (c) 2018-2021 Emil Ernerfeldt 
+Copyright (c) 2024 Rerun Technologies AB 
 
 Permission is hereby granted, free of charge, to any
 person obtaining a copy of this software and associated
diff --git a/rust-toolchain b/rust-toolchain
index 87c96ef..521e293 100644
--- a/rust-toolchain
+++ b/rust-toolchain
@@ -6,5 +6,5 @@
 
 [toolchain]
 channel = "1.73.0"
-components = [ "rustfmt", "clippy" ]
-targets = [ "wasm32-unknown-unknown" ]
+components = ["rustfmt", "clippy"]
+targets = ["wasm32-unknown-unknown"]

From db80de37d376f15919ddcfd11f52e8eb38b23bd8 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:30:08 +0200
Subject: [PATCH 08/19] Add script to generate chagelog

---
 scripts/generate_changelog.py | 191 ++++++++++++++++++++++++++++++++++
 1 file changed, 191 insertions(+)
 create mode 100755 scripts/generate_changelog.py

diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py
new file mode 100755
index 0000000..f47a6b4
--- /dev/null
+++ b/scripts/generate_changelog.py
@@ -0,0 +1,191 @@
+#!/usr/bin/env python3
+# Copied from https://github.com/rerun-io/rerun_template
+
+"""
+Summarizes recent PRs based on their GitHub labels.
+
+The result can be copy-pasted into CHANGELOG.md,
+though it often needs some manual editing too.
+"""
+
+from __future__ import annotations
+
+import argparse
+import multiprocessing
+import os
+import re
+import sys
+from dataclasses import dataclass
+from typing import Any, Optional
+
+import requests
+from git import Repo  # pip install GitPython
+from tqdm import tqdm
+
+OWNER = "rerun-io"
+REPO = "new_repo_name"
+INCLUDE_LABELS = False  # It adds quite a bit of visual noise
+OFFICIAL_RERUN_DEVS = [
+    "abey79",
+    "emilk",
+    "jleibs",
+    "jprochazk",
+    "nikolausWest",
+    "teh-cmc",
+    "Wumpf",
+]
+
+
+@dataclass
+class PrInfo:
+    gh_user_name: str
+    pr_title: str
+    labels: list[str]
+
+
+@dataclass
+class CommitInfo:
+    hexsha: str
+    title: str
+    pr_number: Optional[int]
+
+
+def get_github_token() -> str:
+    token = os.environ.get("GH_ACCESS_TOKEN", "")
+    if token != "":
+        return token
+
+    home_dir = os.path.expanduser("~")
+    token_file = os.path.join(home_dir, ".githubtoken")
+
+    try:
+        with open(token_file, encoding="utf8") as f:
+            token = f.read().strip()
+        return token
+    except Exception:
+        pass
+
+    print("ERROR: expected a GitHub token in the environment variable GH_ACCESS_TOKEN or in ~/.githubtoken")
+    sys.exit(1)
+
+
+# Slow
+def fetch_pr_info_from_commit_info(commit_info: CommitInfo) -> Optional[PrInfo]:
+    if commit_info.pr_number is None:
+        return None
+    else:
+        return fetch_pr_info(commit_info.pr_number)
+
+
+# Slow
+def fetch_pr_info(pr_number: int) -> Optional[PrInfo]:
+    url = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}"
+    gh_access_token = get_github_token()
+    headers = {"Authorization": f"Token {gh_access_token}"}
+    response = requests.get(url, headers=headers)
+    json = response.json()
+
+    # Check if the request was successful (status code 200)
+    if response.status_code == 200:
+        labels = [label["name"] for label in json["labels"]]
+        gh_user_name = json["user"]["login"]
+        return PrInfo(gh_user_name=gh_user_name, pr_title=json["title"], labels=labels)
+    else:
+        print(f"ERROR {url}: {response.status_code} - {json['message']}")
+        return None
+
+
+def get_commit_info(commit: Any) -> CommitInfo:
+    match = re.match(r"(.*) \(#(\d+)\)", commit.summary)
+    if match:
+        title = str(match.group(1))
+        pr_number = int(match.group(2))
+        return CommitInfo(hexsha=commit.hexsha, title=title, pr_number=pr_number)
+    else:
+        return CommitInfo(hexsha=commit.hexsha, title=commit.summary, pr_number=None)
+
+
+def remove_prefix(text: str, prefix: str) -> str:
+    if text.startswith(prefix):
+        return text[len(prefix) :]
+    return text  # or whatever
+
+
+def print_section(crate: str, items: list[str]) -> None:
+    if 0 < len(items):
+        print(f"#### {crate}")
+        for line in items:
+            print(f"* {line}")
+    print()
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description="Generate a changelog.")
+    parser.add_argument("--commit-range", help="e.g. 0.1.0..HEAD", required=True)
+    args = parser.parse_args()
+
+    repo = Repo(".")
+    commits = list(repo.iter_commits(args.commit_range))
+    commits.reverse()  # Most recent last
+    commit_infos = list(map(get_commit_info, commits))
+
+    pool = multiprocessing.Pool()
+    pr_infos = list(
+        tqdm(
+            pool.imap(fetch_pr_info_from_commit_info, commit_infos),
+            total=len(commit_infos),
+            desc="Fetch PR info commits",
+        )
+    )
+
+    prs = []
+    unsorted_commits = []
+
+    for commit_info, pr_info in zip(commit_infos, pr_infos):
+        hexsha = commit_info.hexsha
+        title = commit_info.title
+        title = title.rstrip(".").strip()  # Some PR end with an unnecessary period
+        pr_number = commit_info.pr_number
+
+        if pr_number is None:
+            # Someone committed straight to main:
+            summary = f"{title} [{hexsha[:7]}](https://github.com/{OWNER}/{REPO}/commit/{hexsha})"
+            unsorted_commits.append(summary)
+        else:
+            # We prefer the PR title if available
+            title = pr_info.pr_title if pr_info else title
+            labels = pr_info.labels if pr_info else []
+
+            if "exclude from changelog" in labels:
+                continue
+            if "typo" in labels:
+                # We get so many typo PRs. Let's not flood the changelog with them.
+                continue
+
+            summary = f"{title} [#{pr_number}](https://github.com/{OWNER}/{REPO}/pull/{pr_number})"
+
+            if INCLUDE_LABELS and 0 < len(labels):
+                summary += f" ({', '.join(labels)})"
+
+            if pr_info is not None:
+                gh_user_name = pr_info.gh_user_name
+                if gh_user_name not in OFFICIAL_RERUN_DEVS:
+                    summary += f" (thanks [@{gh_user_name}](https://github.com/{gh_user_name})!)"
+
+            prs.append(summary)
+
+    # Clean up:
+    for i in range(len(prs)):
+        line = prs[i]
+        line = line[0].upper() + line[1:]  # Upper-case first letter
+        prs[i] = line
+
+    print()
+    print(f"Full diff at https://github.com/rerun-io/{REPO}/compare/{args.commit_range}")
+    print()
+    print_section("PRs", prs)
+    print_section("Unsorted commits", unsorted_commits)
+
+
+if __name__ == "__main__":
+    main()

From 20d999e0adac346170ad9eabeba3d26479c496cb Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:30:20 +0200
Subject: [PATCH 09/19] Add script to update repository from template

---
 scripts/template_update.py | 167 +++++++++++++++++++++++++++++++++++++
 1 file changed, 167 insertions(+)
 create mode 100755 scripts/template_update.py

diff --git a/scripts/template_update.py b/scripts/template_update.py
new file mode 100755
index 0000000..85e3110
--- /dev/null
+++ b/scripts/template_update.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python3
+# Copied from https://github.com/rerun-io/rerun_template
+
+"""
+The script has two purposes.
+
+After using `rerun_template` as a template, run this to clean out things you don't need.
+Use `scripts/template_update.py init --languages cpp,rust,python` for this.
+
+Update an existing repository with the latest changes from the template.
+Use `scripts/template_update.py update --languages cpp,rust,python` for this.
+
+In either case, make sure the list of languages matches the languages you want to support.
+You can also use `--dry-run` to see what would happen without actually changing anything.
+"""
+
+from __future__ import annotations
+
+import argparse
+import os
+import shutil
+import tempfile
+
+from git import Repo
+
+OWNER = "rerun-io"
+
+# Files requires by C++, but not by both Python or Rust.
+CPP_FILES = {
+    ".clang-format",
+    ".github/workflows/cpp.yml",
+    "CMakeLists.txt",
+    "pixi.lock",  # Not needed by Rust
+    "pixi.toml",  # Not needed by Rust
+    "src/main.cpp",
+    "src/",
+}
+
+# Files requires by Python, but not by both C++ or Rust
+PYTHON_FILES = {
+    ".github/workflows/python.yml",
+    ".mypy.ini",
+    "main.py",
+    "pixi.lock",  # Not needed by Rust
+    "pixi.toml",  # Not needed by Rust
+    "pyproject.toml",
+    "requirements.txt",
+}
+
+# Files requires by Rust, but not by both C++ or Python
+RUST_FILES = {
+    ".github/workflows/rust.yml",
+    "bacon.toml",
+    "Cargo.lock",
+    "Cargo.toml",
+    "clippy.toml",
+    "Cranky.toml",
+    "deny.toml",
+    "rust-toolchain",
+    "scripts/clippy_wasm/",
+    "scripts/clippy_wasm/clippy.toml",
+    "src/lib.rs",
+    "src/main.rs",
+    "src/",
+}
+
+
+def parse_languages(lang_str: str) -> set[str]:
+    languages = lang_str.split(",") if lang_str else []
+    for lang in languages:
+        assert lang in ["cpp", "python", "rust"], f"Unsupported language: {lang}"
+    return set(languages)
+
+
+def calc_deny_set(languages: set[str]) -> set[str]:
+    """The set of files to delete/ignore."""
+    files_to_delete = CPP_FILES | PYTHON_FILES | RUST_FILES
+    if "cpp" in languages:
+        files_to_delete -= CPP_FILES
+    if "python" in languages:
+        files_to_delete -= PYTHON_FILES
+    if "rust" in languages:
+        files_to_delete -= RUST_FILES
+    return files_to_delete
+
+
+def init(languages: set[str], dry_run: bool) -> None:
+    print("Removing all language-specific files not needed for languages {languages}.")
+    files_to_delete = calc_deny_set(languages)
+    delete_files_and_folder(files_to_delete, dry_run)
+
+
+def delete_files_and_folder(paths: set[str], dry_run: bool) -> None:
+    repo_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+    for path in paths:
+        full_path = os.path.join(repo_path, path)
+        if os.path.exists(full_path):
+            if os.path.isfile(full_path):
+                print(f"Removing file {full_path}…")
+                if not dry_run:
+                    os.remove(full_path)
+            elif os.path.isdir(full_path):
+                print(f"Removing folder {full_path}…")
+                if not dry_run:
+                    shutil.rmtree(full_path)
+
+
+def update(languages: set[str], dry_run: bool) -> None:
+    # Don't overwrite these
+    ALWAYS_IGNORE_FILES = {"README.md", "pixi.lock", "Cargo.lock", "main.py", "requirements.txt"}
+
+    files_to_ignore = calc_deny_set(languages) | ALWAYS_IGNORE_FILES
+    repo_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+
+    with tempfile.TemporaryDirectory() as temp_dir:
+        Repo.clone_from("https://github.com/rerun-io/rerun_template.git", temp_dir)
+        for root, dirs, files in os.walk(temp_dir):
+            for file in files:
+                src_path = os.path.join(root, file)
+                rel_path = os.path.relpath(src_path, temp_dir)
+
+                if rel_path.startswith(".git/"):
+                    continue
+                if rel_path.startswith("src/"):
+                    continue
+                if rel_path in files_to_ignore:
+                    continue
+
+                dest_path = os.path.join(repo_path, rel_path)
+
+                print(f"Updating {rel_path}…")
+                if not dry_run:
+                    os.makedirs(os.path.dirname(dest_path), exist_ok=True)
+                    shutil.copy2(src_path, dest_path)
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description="Handle the Rerun template.")
+    subparsers = parser.add_subparsers(dest="command")
+
+    init_parser = subparsers.add_parser("init", help="Initialize a new checkout of the template.")
+    init_parser.add_argument(
+        "--languages", default="", nargs="?", const="", help="The languages to support (e.g. `cpp,python,rust`)."
+    )
+    init_parser.add_argument("--dry-run", action="store_true", help="Don't actually delete any files.")
+
+    update_parser = subparsers.add_parser(
+        "update", help="Update all existing Rerun repositories with the latest changes from the template"
+    )
+    update_parser.add_argument(
+        "--languages", default="", nargs="?", const="", help="The languages to support (e.g. `cpp,python,rust`)."
+    )
+    update_parser.add_argument("--dry-run", action="store_true", help="Don't actually delete any files.")
+
+    args = parser.parse_args()
+
+    if args.command == "init":
+        init(parse_languages(args.languages), args.dry_run)
+    elif args.command == "update":
+        update(parse_languages(args.languages), args.dry_run)
+    else:
+        parser.print_help()
+        exit(1)
+
+
+if __name__ == "__main__":
+    main()

From 7b6a1ba6506365c9f93e29ab4e3524e38503ce81 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:30:29 +0200
Subject: [PATCH 10/19] CI: check for labels

---
 .github/workflows/labels.yml | 34 ++++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)
 create mode 100644 .github/workflows/labels.yml

diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml
new file mode 100644
index 0000000..2c67f7b
--- /dev/null
+++ b/.github/workflows/labels.yml
@@ -0,0 +1,34 @@
+# Copied from https://github.com/rerun-io/rerun_template
+
+# https://github.com/marketplace/actions/require-labels
+# Check for existence of labels
+# See all our labels at https://github.com/rerun-io/rerun/issues/labels
+
+name: PR Labels
+
+on:
+  pull_request:
+    types:
+      - opened
+      - synchronize
+      - reopened
+      - labeled
+      - unlabeled
+
+jobs:
+  label:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check for a "do-not-merge" label
+        uses: mheap/github-action-required-labels@v3
+        with:
+          mode: exactly
+          count: 0
+          labels: "do-not-merge"
+
+      - name: Require label "include in changelog" or "exclude from changelog"
+        uses: mheap/github-action-required-labels@v3
+        with:
+          mode: minimum
+          count: 1
+          labels: "exclude from changelog, include in changelog"

From 993dfd7497323cb8e2bdebfdc3495ad8050c6186 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:30:40 +0200
Subject: [PATCH 11/19] Unify Rust CI

---
 .github/workflows/rust.yml | 101 ++++++++++++++++++++++++-------------
 1 file changed, 67 insertions(+), 34 deletions(-)

diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index c901090..b765140 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -1,60 +1,67 @@
+# Copied from https://github.com/rerun-io/rerun_template
 on: [push, pull_request]
 
-name: CI
+name: Rust
 
 env:
-  # This is required to enable the web_sys clipboard API which egui_web uses
+  # --cfg=web_sys_unstable_apis is required to enable the web_sys clipboard API which egui_web uses
   # https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
   # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
-  RUSTFLAGS: --cfg=web_sys_unstable_apis
+  RUSTFLAGS: -D warnings --cfg=web_sys_unstable_apis
+  RUSTDOCFLAGS: -D warnings
 
 jobs:
-  check:
-    name: Rust format, cranky, check, test, doc
+  rust-check:
+    name: Rust
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
+
       - uses: actions-rs/toolchain@v1
         with:
-          profile: minimal
+          profile: default
           toolchain: 1.73.0
           override: true
-          components: rustfmt, clippy
 
-      - run: |
-          sudo apt-get update
-          sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev
+      - name: Install packages (Linux)
+        uses: awalsh128/cache-apt-pkgs-action@v1.3.0
+        with:
+          # Some deps used by eframe:
+          packages: libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
+          version: 1.0
+          execute_install_scripts: true
 
       - name: Set up cargo cache
         uses: Swatinem/rust-cache@v2
 
-      - name: Install cargo-cranky
-        uses: baptiste0928/cargo-install@v1
-        with:
-          crate: cargo-cranky
-
       - name: Rustfmt
         uses: actions-rs/cargo@v1
         with:
           command: fmt
           args: --all -- --check
 
-      - name: Cranky
+      - name: Install cargo-cranky
+        uses: baptiste0928/cargo-install@v1
+        with:
+          crate: cargo-cranky
+
+      - name: check --all-features
         uses: actions-rs/cargo@v1
         with:
-          command: cranky
-          args: --all-targets --all-features --  -D warnings
+          command: check
+          args: --all-features --all-targets
 
-      - name: Check
+      - name: check default features
         uses: actions-rs/cargo@v1
         with:
           command: check
+          args: --all-targets
 
-      - name: Check --all-features
+      - name: check --no-default-features
         uses: actions-rs/cargo@v1
         with:
           command: check
-          args: --all-features
+          args: --no-default-features --lib --all-targets
 
       - name: Test doc-tests
         uses: actions-rs/cargo@v1
@@ -62,11 +69,11 @@ jobs:
           command: test
           args: --doc --all-features
 
-      - name: cargo doc
+      - name: cargo doc --lib
         uses: actions-rs/cargo@v1
         with:
           command: doc
-          args: --no-deps --all-features
+          args: --lib --no-deps --all-features
 
       - name: cargo doc --document-private-items
         uses: actions-rs/cargo@v1
@@ -74,17 +81,31 @@ jobs:
           command: doc
           args: --document-private-items --no-deps --all-features
 
-      - name: Test
+      - name: Build tests
         uses: actions-rs/cargo@v1
         with:
           command: test
-          args: --all-features --lib
+          args: --all-features --no-run
+
+      - name: Run test
+        uses: actions-rs/cargo@v1
+        with:
+          command: test
+          args: --all-features
+
+      - name: Cranky
+        uses: actions-rs/cargo@v1
+        with:
+          command: cranky
+          args: --all-targets --all-features -- -D warnings
+
+  # ---------------------------------------------------------------------------
 
   check_wasm:
     name: Check wasm32
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
       - uses: actions-rs/toolchain@v1
         with:
           profile: minimal
@@ -95,19 +116,31 @@ jobs:
       - name: Set up cargo cache
         uses: Swatinem/rust-cache@v2
 
+      - name: Install cargo-cranky
+        uses: baptiste0928/cargo-install@v1
+        with:
+          crate: cargo-cranky
+
       - name: Check wasm32
         uses: actions-rs/cargo@v1
         with:
           command: check
-          args: --all-features --lib --target wasm32-unknown-unknown
+          args: --target wasm32-unknown-unknown
+
+      - name: Cranky wasm32
+        env:
+          CLIPPY_CONF_DIR: "scripts/clippy_wasm" # Use scripts/clippy_wasm/clippy.toml
+        run: cargo cranky --target wasm32-unknown-unknown -- -D warnings
+
+  # ---------------------------------------------------------------------------
 
   cargo-deny:
     name: Check Rust dependencies (cargo-deny)
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-
-      - uses: EmbarkStudios/cargo-deny-action@v1
-        with:
-          rust-version: "1.73.0"
-          log-level: error
+    - uses: actions/checkout@v3
+    - uses: EmbarkStudios/cargo-deny-action@v1
+      with:
+        rust-version: "1.73.0"
+        log-level: warn
+        command: check

From f6d01125105055193cabeabee81fb0417f156762 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:34:35 +0200
Subject: [PATCH 12/19] More agressive clippy

---
 Cranky.toml                              | 53 +++++++++++++---
 clippy.toml                              | 79 ++++++++++++++++++++++++
 echo_server/src/main.rs                  |  1 +
 ewebsock/src/native_tungstenite.rs       |  4 ++
 ewebsock/src/native_tungstenite_tokio.rs |  4 ++
 ewebsock/src/web.rs                      |  3 +
 example_app/src/app.rs                   |  2 +-
 scripts/clippy_wasm/clippy.toml          | 75 ++++++++++++++++++++++
 8 files changed, 211 insertions(+), 10 deletions(-)
 create mode 100644 clippy.toml
 create mode 100644 scripts/clippy_wasm/clippy.toml

diff --git a/Cranky.toml b/Cranky.toml
index 93f9ed3..69cc8bd 100644
--- a/Cranky.toml
+++ b/Cranky.toml
@@ -1,23 +1,32 @@
+# Copied from https://github.com/rerun-io/rerun_template
+#
 # https://github.com/ericseppanen/cargo-cranky
 # cargo install cargo-cranky && cargo cranky
+# See also clippy.toml
 
 deny = [
   "unsafe_code",
-  # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602
-  #"clippy::self_named_module_files",
+  #"clippy::self_named_module_files", # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602
 ]
 
 warn = [
   "clippy::all",
+  "clippy::as_ptr_cast_mut",
   "clippy::await_holding_lock",
   "clippy::bool_to_int_with_if",
   "clippy::char_lit_as_u8",
   "clippy::checked_conversions",
+  "clippy::clear_with_drain",
+  "clippy::cloned_instead_of_copied",
+  "clippy::cloned_instead_of_copied",
   "clippy::dbg_macro",
   "clippy::debug_assert_with_mut_call",
   "clippy::derive_partial_eq_without_eq",
-  "clippy::disallowed_methods",
-  "clippy::disallowed_script_idents",
+  "clippy::disallowed_macros",                  # See clippy.toml
+  "clippy::disallowed_methods",                 # See clippy.toml
+  "clippy::disallowed_names",                   # See clippy.toml
+  "clippy::disallowed_script_idents",           # See clippy.toml
+  "clippy::disallowed_types",                   # See clippy.toml
   "clippy::doc_link_with_quotes",
   "clippy::doc_markdown",
   "clippy::empty_enum",
@@ -35,6 +44,7 @@ warn = [
   "clippy::fn_params_excessive_bools",
   "clippy::fn_to_numeric_cast_any",
   "clippy::from_iter_instead_of_collect",
+  "clippy::get_unwrap",
   "clippy::if_let_mutex",
   "clippy::implicit_clone",
   "clippy::imprecise_flops",
@@ -45,14 +55,19 @@ warn = [
   "clippy::iter_on_empty_collections",
   "clippy::iter_on_single_items",
   "clippy::large_digit_groups",
+  "clippy::large_include_file",
   "clippy::large_stack_arrays",
+  "clippy::large_stack_frames",
   "clippy::large_types_passed_by_value",
+  "clippy::let_underscore_untyped",
   "clippy::let_unit_value",
   "clippy::linkedlist",
   "clippy::lossy_float_literal",
   "clippy::macro_use_imports",
   "clippy::manual_assert",
+  "clippy::manual_clamp",
   "clippy::manual_instant_elapsed",
+  "clippy::manual_let_else",
   "clippy::manual_ok_or",
   "clippy::manual_string_new",
   "clippy::map_err_ignore",
@@ -65,42 +80,64 @@ warn = [
   "clippy::mem_forget",
   "clippy::mismatched_target_os",
   "clippy::mismatching_type_param_order",
+  "clippy::missing_assert_message",
   "clippy::missing_enforced_import_renames",
+  "clippy::missing_errors_doc",
   "clippy::missing_safety_doc",
   "clippy::mut_mut",
   "clippy::mutex_integer",
   "clippy::needless_borrow",
   "clippy::needless_continue",
   "clippy::needless_for_each",
+  "clippy::needless_pass_by_ref_mut",
   "clippy::needless_pass_by_value",
   "clippy::negative_feature_names",
   "clippy::nonstandard_macro_braces",
   "clippy::option_option",
   "clippy::path_buf_push_overwrite",
   "clippy::ptr_as_ptr",
+  "clippy::ptr_cast_constness",
+  "clippy::pub_without_shorthand",
   "clippy::rc_mutex",
+  "clippy::readonly_write_lock",
+  "clippy::redundant_type_annotations",
   "clippy::ref_option_ref",
+  "clippy::ref_patterns",
   "clippy::rest_pat_in_fully_bound_structs",
   "clippy::same_functions_in_if_condition",
   "clippy::semicolon_if_nothing_returned",
+  "clippy::significant_drop_tightening",
   "clippy::single_match_else",
   "clippy::str_to_string",
   "clippy::string_add_assign",
   "clippy::string_add",
   "clippy::string_lit_as_bytes",
+  "clippy::string_lit_chars_any",
   "clippy::string_to_string",
+  "clippy::suspicious_command_arg_space",
+  "clippy::suspicious_xor_used_as_pow",
   "clippy::todo",
+  "clippy::too_many_lines",
   "clippy::trailing_empty_array",
   "clippy::trait_duplication_in_bounds",
+  "clippy::tuple_array_conversions",
+  "clippy::unchecked_duration_subtraction",
   "clippy::undocumented_unsafe_blocks",
   "clippy::unimplemented",
+  "clippy::uninlined_format_args",
+  "clippy::unnecessary_box_returns",
+  "clippy::unnecessary_safety_doc",
+  "clippy::unnecessary_struct_initialization",
   "clippy::unnecessary_wraps",
   "clippy::unnested_or_patterns",
   "clippy::unused_peekable",
   "clippy::unused_rounding",
   "clippy::unused_self",
+  "clippy::unwrap_used",
   "clippy::useless_transmute",
   "clippy::verbose_file_reads",
+  "clippy::wildcard_dependencies",
+  "clippy::wildcard_imports",
   "clippy::zero_sized_map_values",
   "elided_lifetimes_in_paths",
   "future_incompatible",
@@ -109,17 +146,15 @@ warn = [
   "rust_2021_prelude_collisions",
   "rustdoc::missing_crate_level_docs",
   "semicolon_in_expressions_from_macros",
+  "trivial_casts",
   "trivial_numeric_casts",
   "unsafe_op_in_unsafe_fn",                     # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668
   "unused_extern_crates",
   "unused_import_braces",
   "unused_lifetimes",
+  "unused_qualifications",
 ]
 
 allow = [
-  # TODO(emilk): enable more lints
-  "clippy::cloned_instead_of_copied",
-  "clippy::missing_errors_doc",
-  "unused_qualifications",
-  "trivial_casts",
+  "clippy::manual_range_contains", # this one is just worse imho
 ]
diff --git a/clippy.toml b/clippy.toml
new file mode 100644
index 0000000..d2a61d6
--- /dev/null
+++ b/clippy.toml
@@ -0,0 +1,79 @@
+# Copied from https://github.com/rerun-io/rerun_template
+#
+# There is also a scripts/clippy_wasm/clippy.toml which forbids some methods that are not available in wasm.
+
+# -----------------------------------------------------------------------------
+# Section identical to scripts/clippy_wasm/clippy.toml:
+
+msrv = "1.73"
+
+allow-unwrap-in-tests = true
+
+# https://doc.rust-lang.org/nightly/clippy/lint_configuration.html#avoid-breaking-exported-api
+# We want suggestions, even if it changes public API.
+avoid-breaking-exported-api = false
+
+excessive-nesting-threshold = 8
+
+max-fn-params-bools = 1
+
+# https://rust-lang.github.io/rust-clippy/master/index.html#/large_include_file
+max-include-file-size = 1000000
+
+# https://rust-lang.github.io/rust-clippy/master/index.html#/large_stack_frames
+stack-size-threshold = 512000
+
+too-many-lines-threshold = 200
+
+# -----------------------------------------------------------------------------
+
+# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_macros
+disallowed-macros = ['dbg']
+
+# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods
+disallowed-methods = [
+  { path = "egui_extras::TableBody::row", reason = "`row` doesn't scale. Use `rows` instead." },
+  { path = "glam::Vec2::normalize", reason = "normalize() can create NaNs. Use try_normalize or normalize_or_zero" },
+  { path = "glam::Vec3::normalize", reason = "normalize() can create NaNs. Use try_normalize or normalize_or_zero" },
+  { path = "sha1::Digest::new", reason = "SHA1 is cryptographically broken" },
+  { path = "std::env::temp_dir", reason = "Use the tempdir crate instead" },
+  { path = "std::panic::catch_unwind", reason = "We compile with `panic = 'abort'`" },
+  { path = "std::thread::spawn", reason = "Use `std::thread::Builder` and name the thread" },
+
+  # There are many things that aren't allowed on wasm,
+  # but we cannot disable them all here (because of e.g. https://github.com/rust-lang/rust-clippy/issues/10406)
+  # so we do that in `scripts/clippy_wasm/clippy.toml` instead.
+]
+
+# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_names
+disallowed-names = []
+
+# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types
+disallowed-types = [
+  { path = "ring::digest::SHA1_FOR_LEGACY_USE_ONLY", reason = "SHA1 is cryptographically broken" },
+
+  { path = "std::sync::Condvar", reason = "Use parking_lot instead" },
+  { path = "std::sync::Mutex", reason = "Use parking_lot instead" },
+  { path = "std::sync::RwLock", reason = "Use parking_lot instead" },
+
+  # "std::sync::Once",  # enabled for now as the `log_once` macro uses it internally
+]
+
+# Allow-list of words for markdown in dosctrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
+doc-valid-idents = [
+  # You must also update the same list in `scripts/clippy_wasm/clippy.toml`!
+  "GitHub",
+  "GLB",
+  "GLTF",
+  "iOS",
+  "macOS",
+  "NaN",
+  "OBJ",
+  "OpenGL",
+  "PyPI",
+  "sRGB",
+  "sRGBA",
+  "WebGL",
+  "WebSocket",
+  "WebSockets",
+]
diff --git a/echo_server/src/main.rs b/echo_server/src/main.rs
index f758b8f..0eefd3c 100644
--- a/echo_server/src/main.rs
+++ b/echo_server/src/main.rs
@@ -1,4 +1,5 @@
 #![allow(deprecated)] // TODO(emilk): Remove when we update tungstenite
+#![allow(clippy::unwrap_used, clippy::disallowed_methods)] // We are just testing here.
 
 use std::{net::TcpListener, thread::spawn};
 
diff --git a/ewebsock/src/native_tungstenite.rs b/ewebsock/src/native_tungstenite.rs
index e2e38cb..deec026 100644
--- a/ewebsock/src/native_tungstenite.rs
+++ b/ewebsock/src/native_tungstenite.rs
@@ -32,6 +32,10 @@ impl WsSender {
     /// Close the connection.
     ///
     /// This is called automatically when the sender is dropped.
+    ///
+    /// # Errors
+    /// This should never fail, except _maybe_ on Web.
+    #[allow(clippy::unnecessary_wraps)] // To keep the same signature as the Web version
     pub fn close(&mut self) -> Result<()> {
         if self.tx.is_some() {
             log::debug!("Closing WebSocket");
diff --git a/ewebsock/src/native_tungstenite_tokio.rs b/ewebsock/src/native_tungstenite_tokio.rs
index 63906f7..f6afa3d 100644
--- a/ewebsock/src/native_tungstenite_tokio.rs
+++ b/ewebsock/src/native_tungstenite_tokio.rs
@@ -28,6 +28,10 @@ impl WsSender {
     /// Close the connection.
     ///
     /// This is called automatically when the sender is dropped.
+    ///
+    /// # Errors
+    /// This should never fail, except _maybe_ on Web.
+    #[allow(clippy::unnecessary_wraps)] // To keep the same signature as the Web version
     pub fn close(&mut self) -> Result<()> {
         if self.tx.is_some() {
             log::debug!("Closing WebSocket");
diff --git a/ewebsock/src/web.rs b/ewebsock/src/web.rs
index ae9a81b..e9dfe94 100644
--- a/ewebsock/src/web.rs
+++ b/ewebsock/src/web.rs
@@ -48,6 +48,9 @@ impl WsSender {
     /// Close the connection.
     ///
     /// This is called automatically when the sender is dropped.
+    ///
+    /// # Errors
+    /// This should never fail, except _maybe_ on Web.
     pub fn close(&mut self) -> Result<()> {
         if let Some(ws) = self.ws.take() {
             log::debug!("Closing WebSocket");
diff --git a/example_app/src/app.rs b/example_app/src/app.rs
index 6ce5926..2fa9101 100644
--- a/example_app/src/app.rs
+++ b/example_app/src/app.rs
@@ -112,7 +112,7 @@ impl FrontEnd {
             ui.separator();
             ui.heading("Received events:");
             for event in &self.events {
-                ui.label(format!("{:?}", event));
+                ui.label(format!("{event:?}"));
             }
         });
     }
diff --git a/scripts/clippy_wasm/clippy.toml b/scripts/clippy_wasm/clippy.toml
new file mode 100644
index 0000000..4cc1768
--- /dev/null
+++ b/scripts/clippy_wasm/clippy.toml
@@ -0,0 +1,75 @@
+# Copied from https://github.com/rerun-io/rerun_template
+
+# This is used by the CI so we can forbid some methods that are not available in wasm.
+#
+# We cannot forbid all these methods in the main `clippy.toml` because of
+# https://github.com/rust-lang/rust-clippy/issues/10406
+
+# -----------------------------------------------------------------------------
+# Section identical to the main clippy.toml:
+
+msrv = "1.73"
+
+allow-unwrap-in-tests = true
+
+# https://doc.rust-lang.org/nightly/clippy/lint_configuration.html#avoid-breaking-exported-api
+# We want suggestions, even if it changes public API.
+avoid-breaking-exported-api = false
+
+excessive-nesting-threshold = 8
+
+max-fn-params-bools = 1
+
+# https://rust-lang.github.io/rust-clippy/master/index.html#/large_include_file
+max-include-file-size = 1000000
+
+too-many-lines-threshold = 200
+
+# -----------------------------------------------------------------------------
+
+# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods
+disallowed-methods = [
+  { path = "crossbeam::channel::Receiver::into_iter", reason = "Cannot block on Web" },
+  { path = "crossbeam::channel::Receiver::iter", reason = "Cannot block on Web" },
+  { path = "crossbeam::channel::Receiver::recv_timeout", reason = "Cannot block on Web" },
+  { path = "crossbeam::channel::Receiver::recv", reason = "Cannot block on Web" },
+  { path = "poll_promise::Promise::block_and_take", reason = "Cannot block on Web" },
+  { path = "poll_promise::Promise::block_until_ready_mut", reason = "Cannot block on Web" },
+  { path = "poll_promise::Promise::block_until_ready", reason = "Cannot block on Web" },
+  { path = "rayon::spawn", reason = "Cannot spawn threads on wasm" },
+  { path = "std::sync::mpsc::Receiver::into_iter", reason = "Cannot block on Web" },
+  { path = "std::sync::mpsc::Receiver::iter", reason = "Cannot block on Web" },
+  { path = "std::sync::mpsc::Receiver::recv_timeout", reason = "Cannot block on Web" },
+  { path = "std::sync::mpsc::Receiver::recv", reason = "Cannot block on Web" },
+  { path = "std::thread::spawn", reason = "Cannot spawn threads on wasm" },
+  { path = "std::time::Duration::elapsed", reason = "use `web-time` crate instead for wasm/web compatibility" },
+  { path = "std::time::Instant::now", reason = "use `web-time` crate instead for wasm/web compatibility" },
+  { path = "std::time::SystemTime::now", reason = "use `web-time` or `time` crates instead for wasm/web compatibility" },
+]
+
+# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types
+disallowed-types = [
+  { path = "instant::SystemTime", reason = "Known bugs. Use web-time." },
+  { path = "std::thread::Builder", reason = "Cannot spawn threads on wasm" },
+  # { path = "std::path::PathBuf", reason = "Can't read/write files on web" }, // Used in build.rs files (which is fine).
+]
+
+# Allow-list of words for markdown in dosctrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
+doc-valid-idents = [
+  # You must also update the same list in the root `clippy.toml`!
+  "..",
+  "GitHub",
+  "GLB",
+  "GLTF",
+  "iOS",
+  "macOS",
+  "NaN",
+  "OBJ",
+  "OpenGL",
+  "PyPI",
+  "sRGB",
+  "sRGBA",
+  "WebGL",
+  "WebSocket",
+  "WebSockets",
+]

From 096ac9f06f3af83e8a43940307a172b0f5d235d8 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:44:14 +0200
Subject: [PATCH 13/19] Fix doclink

---
 ewebsock/src/native_tungstenite_tokio.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ewebsock/src/native_tungstenite_tokio.rs b/ewebsock/src/native_tungstenite_tokio.rs
index f6afa3d..29b3e50 100644
--- a/ewebsock/src/native_tungstenite_tokio.rs
+++ b/ewebsock/src/native_tungstenite_tokio.rs
@@ -127,7 +127,7 @@ pub(crate) fn ws_connect_impl(
     Ok(ws_connect_native(url, options, on_event))
 }
 
-/// Like [`ws_connect`], but cannot fail. Only available on native builds.
+/// Like [`crate::ws_connect`], but cannot fail. Only available on native builds.
 fn ws_connect_native(url: String, options: Options, on_event: EventHandler) -> WsSender {
     let (tx, mut rx) = tokio::sync::mpsc::channel(1000);
 

From bab981a57478f3c991405379ad32e89d188f4c3a Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:44:49 +0200
Subject: [PATCH 14/19] Remove dead link

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c89739..ba7d0f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,7 +19,7 @@
 * Fix: On web, close connection when dropping `WsSender` (#8)
 
 
-## [0.2.0](https://github.com/rerun-io/ewebsock/compare/0.1.0...0.2.0) - 2022-04-08
+## 0.2.0 - 2022-04-08
 * Support WSS (WebSocket Secure) / TLS.
 * Improve error reporting.
 * `EventHandler` no longer needs to be `Sync`.

From b9ea86289f139045d765dd4ac4d067c70f4b7672 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:50:02 +0200
Subject: [PATCH 15/19] Only check libraries for wasm32 support

---
 .github/workflows/rust.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index b765140..053a581 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -125,12 +125,12 @@ jobs:
         uses: actions-rs/cargo@v1
         with:
           command: check
-          args: --target wasm32-unknown-unknown
+          args: --target wasm32-unknown-unknown --lib
 
       - name: Cranky wasm32
         env:
           CLIPPY_CONF_DIR: "scripts/clippy_wasm" # Use scripts/clippy_wasm/clippy.toml
-        run: cargo cranky --target wasm32-unknown-unknown -- -D warnings
+        run: cargo cranky --target wasm32-unknown-unknown --lib -- -D warnings
 
   # ---------------------------------------------------------------------------
 

From a38d62d4277a9e86576fedc2853bb3ff598e8aed Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 10:59:09 +0200
Subject: [PATCH 16/19] Fix some Wasm clippy lints

---
 ewebsock/src/web.rs | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/ewebsock/src/web.rs b/ewebsock/src/web.rs
index e9dfe94..06290da 100644
--- a/ewebsock/src/web.rs
+++ b/ewebsock/src/web.rs
@@ -1,13 +1,15 @@
+#![allow(trivial_casts)]
+
 use crate::{EventHandler, Options, Result, WsEvent, WsMessage};
 
 #[allow(clippy::needless_pass_by_value)]
 fn string_from_js_value(s: wasm_bindgen::JsValue) -> String {
-    s.as_string().unwrap_or(format!("{:#?}", s))
+    s.as_string().unwrap_or(format!("{s:#?}"))
 }
 
 #[allow(clippy::needless_pass_by_value)]
 fn string_from_js_string(s: js_sys::JsString) -> String {
-    s.as_string().unwrap_or(format!("{:#?}", s))
+    s.as_string().unwrap_or(format!("{s:#?}"))
 }
 
 /// This is how you send messages to the server.
@@ -36,11 +38,11 @@ impl WsSender {
                 }
                 WsMessage::Text(text) => ws.send_with_str(&text),
                 unknown => {
-                    panic!("Don't know how to send message: {:?}", unknown);
+                    panic!("Don't know how to send message: {unknown:?}");
                 }
             };
             if let Err(err) = result.map_err(string_from_js_value) {
-                log::error!("Failed to send: {:?}", err);
+                log::error!("Failed to send: {err:?}");
             }
         }
     }
@@ -70,6 +72,7 @@ pub(crate) fn ws_receive_impl(url: String, options: Options, on_event: EventHand
     ws_connect_impl(url, options, on_event).map(|sender| sender.forget())
 }
 
+#[allow(clippy::needless_pass_by_value)] // For consistency with the native version
 pub(crate) fn ws_connect_impl(
     url: String,
     _ignored_options: Options,

From b10af49096f16a323c1737c801b351a14986ca86 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 11:02:37 +0200
Subject: [PATCH 17/19] Web: Add error handling when failing to read binary
 blob

---
 ewebsock/src/web.rs | 21 ++++++++++++++++-----
 1 file changed, 16 insertions(+), 5 deletions(-)

diff --git a/ewebsock/src/web.rs b/ewebsock/src/web.rs
index 06290da..8d55f48 100644
--- a/ewebsock/src/web.rs
+++ b/ewebsock/src/web.rs
@@ -107,11 +107,22 @@ pub(crate) fn ws_connect_impl(
                 let file_reader_clone = file_reader.clone();
                 // create onLoadEnd callback
                 let on_event = on_event.clone();
-                let onloadend_cb = Closure::wrap(Box::new(move |_e: web_sys::ProgressEvent| {
-                    let array = js_sys::Uint8Array::new(&file_reader_clone.result().unwrap());
-                    on_event(WsEvent::Message(WsMessage::Binary(array.to_vec())));
-                })
-                    as Box);
+                let onloadend_cb =
+                    Closure::wrap(Box::new(
+                        move |_e: web_sys::ProgressEvent| match file_reader_clone.result() {
+                            Ok(file_reader) => {
+                                let array = js_sys::Uint8Array::new(&file_reader);
+                                on_event(WsEvent::Message(WsMessage::Binary(array.to_vec())));
+                            }
+                            Err(err) => {
+                                on_event(WsEvent::Error(format!(
+                                    "Failed to read binary blob: {}",
+                                    string_from_js_value(err)
+                                )));
+                            }
+                        },
+                    )
+                        as Box);
                 file_reader.set_onloadend(Some(onloadend_cb.as_ref().unchecked_ref()));
                 file_reader
                     .read_as_array_buffer(&blob)

From 05739e65cddb7c9639194c6b95bacab5f0d614b6 Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 11:04:20 +0200
Subject: [PATCH 18/19] another clippy lint fix

---
 example_app/src/web.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/example_app/src/web.rs b/example_app/src/web.rs
index 8d3495e..1bd27eb 100644
--- a/example_app/src/web.rs
+++ b/example_app/src/web.rs
@@ -5,7 +5,7 @@ use eframe::wasm_bindgen::{self, prelude::*};
 /// It loads the app, installs some callbacks, then returns.
 /// You can add more callbacks like this if you want to call in to your code.
 #[wasm_bindgen]
-pub async fn start(canvas_id: &str) -> std::result::Result<(), eframe::wasm_bindgen::JsValue> {
+pub async fn start(canvas_id: &str) -> Result<(), JsValue> {
     // Redirect `log` message to `console.log` and friends:
     eframe::WebLogger::init(log::LevelFilter::Debug).ok();
 

From a63fca48459343b00541a3445e62c8edff591d9e Mon Sep 17 00:00:00 2001
From: Emil Ernerfeldt 
Date: Thu, 18 Apr 2024 11:04:30 +0200
Subject: [PATCH 19/19] Same rust-version everywhere

---
 echo_server/Cargo.toml | 2 +-
 example_app/Cargo.toml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/echo_server/Cargo.toml b/echo_server/Cargo.toml
index 116cf7b..f99114f 100644
--- a/echo_server/Cargo.toml
+++ b/echo_server/Cargo.toml
@@ -3,7 +3,7 @@ name = "echo_server"
 version = "0.1.0"
 authors = ["Emil Ernerfeldt "]
 edition = "2021"
-rust-version = "1.56"
+rust-version = "1.73"
 license = "MIT OR Apache-2.0"
 include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
 publish = false
diff --git a/example_app/Cargo.toml b/example_app/Cargo.toml
index 4217b67..a3c1aee 100644
--- a/example_app/Cargo.toml
+++ b/example_app/Cargo.toml
@@ -3,7 +3,7 @@ name = "example_app"
 version = "0.1.0"
 authors = ["Emil Ernerfeldt "]
 edition = "2021"
-rust-version = "1.56"
+rust-version = "1.73"
 license = "MIT OR Apache-2.0"
 include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
 publish = false