Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Material Symbols & Material Icons — Admin & Architecture Guide

Generated June 2026 from google/material-design-icons at fef175f

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is for engineers, architects, and operators who need to understand how google/material-design-icons is built, not just how to consume it. It documents the repository's true nature — a generated mirror of Google's upstream icon pipeline — the Python toolchain that regenerates it, the CI automation that runs weekly, the asset-versioning model, and the operational concerns of cloning, mirroring, or vendoring this distribution.

Important

This repository is not a conventional software project. It is an automated asset distribution: roughly five million generated icon files plus a small (~19 KB) Python toolchain that downloads and lays them out. There is no application to run, no server, no API of its own. The "internals" worth understanding are the generator scripts and the CI that invokes them.


Table of Contents

  1. Executive Summary
  2. System Architecture
  3. Repository Structure
  4. The Update Toolchain
  5. Asset Versioning Model
  6. CI / Automation
  7. Requirements
  8. Operations
  9. Security Analysis
  10. Code Quality & Patterns
  11. Potential Improvements
  12. Quick Reference

1. Executive Summary

Aspect Detail
Purpose Official distribution of Material Symbols (current) and Material Icons (classic) icon assets
Nature Generated mirror — assets are downloaded from fonts.google.com, not authored here
Source of truth Google Fonts upstream (fonts.google.com/metadata/icons)
Generator update/ Python toolchain (update_repo.py, update_symbols.py, icons.py)
Automation GitHub Actions, weekly cron (Fridays 02:00 UTC), commits regenerated assets to master
Scale ~4.8 million files; ~19 KB of actual source code
Sets 2 (Symbols, Icons) × up to 5 styles × 18 categories × multiple sizes/densities/axes
License Apache License 2.0
Contribution model No icon PRs accepted; assets are upstream-generated

Note

Because assets are generated, the meaningful "code" surface is three short Python files. Everything else is output. Treat the repo as a build artifact store with a build script attached, and most operational decisions follow naturally.


2. System Architecture

The repository sits at the downstream end of an internal Google pipeline. It does not design icons; it harvests already-published assets from the public Google Fonts endpoints and arranges them into a stable, browsable directory tree.

            ┌─────────────────────────────────────────────┐
            │  Google Fonts upstream (internal pipeline)   │
            │  - icon designs, versions, family metadata   │
            └───────────────┬─────────────────────────────┘
                            │  public HTTP endpoints
                            │  fonts.google.com/metadata/icons
                            │  {host}/s/i/...   (per-asset URLs)
                            │  fonts.googleapis.com/css2  (fonts)
                            ▼
   ┌──────────────────────────────────────────────────────────┐
   │  update/ toolchain (this repo)                            │
   │                                                          │
   │  update_repo.py     → classic Material Icons assets       │
   │  update_symbols.py  → current Material Symbols assets      │
   │  icons.py           → font → codepoints extraction         │
   │  current_versions.json → per-icon version gate (skip set)  │
   └───────────────┬──────────────────────────────────────────┘
                   │  writes files into sibling dirs
                   ▼
   ┌──────────────────────────────────────────────────────────┐
   │  Asset tree (committed to git, served to users)           │
   │  src/ png/ android/ ios/ font/ symbols/ variablefont/     │
   └───────────────┬──────────────────────────────────────────┘
                   │  git commit + push (GitHub Actions)
                   ▼
            ┌────────────────────────────┐
            │  GitHub: google/material-   │
            │  design-icons @ master      │
            └────────────────────────────┘
                   │
                   ▼  consumed by
        users, fonts.google.com/icons browser,
        community npm packages, IDE asset tools

Data flow, end to end:

  1. The toolchain fetches the icon metadata index from fonts.google.com/metadata/icons (a JSON document with a 5-character prefix stripped before parsing).
  2. For each icon whose upstream version exceeds the version recorded in current_versions.json, it computes a set of fetch URLs from templated patterns and downloads each asset.
  3. ZIP assets (Android/iOS/PNG bundles) are exploded in place; CSS responses from the Fonts API are followed to the real font binary and downloaded.
  4. For each downloaded font, icons.py enumerates ligatures to emit a .codepoints sidecar mapping icon name → Unicode codepoint.
  5. current_versions.json is rewritten with the new versions, and CI commits the changed assets.

Note

The architecture has no persistent services and no state beyond current_versions.json. That single file is the entire incremental-update memory — it is what lets a weekly run download only what changed.


3. Repository Structure

material-design-icons/
├── README.md                Set overview, usage pointers, update history
├── LICENSE                  Apache 2.0
├── .gitignore               Ignores venvs, node_modules, __pycache__
├── .github/
│   └── workflows/
│       └── main.yml         Weekly CI that runs update_symbols.py
├── update/                  ← the only real "code"
│   ├── update_repo.py       Classic Material Icons generator
│   ├── update_symbols.py    Current Material Symbols generator
│   ├── icons.py             Font → (name, codepoint) extractor
│   ├── requirements.txt     Python deps for the toolchain
│   └── current_versions.json  Per-icon version ledger (large, generated)
│
│   ── Generated asset trees (classic Material Icons) ──
├── src/                     Source SVGs, 24px, 5 styles
├── png/                     Raster PNGs, 18–48dp × 1x/2x/3x
├── android/                 Android vector drawables + density PNGs
├── ios/                     Xcode .xcassets imagesets (1x/2x/3x)
├── font/                    Static fonts (.otf/.ttf) + .codepoints
│
│   ── Generated asset trees (current Material Symbols) ──
├── symbols/
│   ├── web/                 SVG per axis variant
│   ├── android/             Vector XML per axis variant
│   └── ios/                 SVG (fill/grade variants; no weight)
└── variablefont/            Variable fonts (.ttf/.woff2) + .codepoints
Path Tracked code? Role
update/*.py Yes (source) Generators + font extractor
update/requirements.txt Yes (source) Dependency pins
update/current_versions.json Yes (generated) Incremental-update ledger
.github/workflows/main.yml Yes (source) Weekly regeneration CI
src/ png/ android/ ios/ font/ symbols/ variablefont/ No (generated) The published assets

Important

Only update/ and .github/ contain human-authored content. The seven asset directories are machine output. Code review, security scanning, and "what changed" analysis should focus almost entirely on update/ and the workflow file.


4. The Update Toolchain

Both generators share the same shape: define a set of (source URL pattern, destination path pattern) Asset tuples, expand them across every icon × style × size × axis combination, fetch, and post-process. They are built on Google's absl library for flags and app entry.

4.1 update_repo.py — classic Material Icons

Downloads the classic set. Drives off fonts.google.com/metadata/icons?incomplete=1.

Per-icon assets (_ICON_ASSETS) — for each icon, style, and size:

Destination pattern Asset
src/{category}/{name}/{set}/{size}px.svg Source SVG
android/{category}/{name}/{set}/black.zip Android bundle (exploded)
ios/{category}/{name}/{set}/black.zip iOS bundle (exploded)
png/{category}/{name}/{set}/{18,24,36,48}dp.zip PNG bundles (exploded)

Per-style assets (_SET_ASSETS): a CSS request to fonts.googleapis.com/css2?family={set} whose response is parsed to find the real .otf/.ttf URL, which is then downloaded into font/.

Command-line flags (absl):

Flag Default Effect
--skip_existing False Don't re-download assets that already exist locally (zips: skip if already exploded)
--fetch True Master switch; --nofetch dry-runs and reports what would be fetched
--explode_zip_files True Unzip downloaded .zip bundles and delete the archive
--icon_limit N 0 Process at most N icons (0 = all). Useful for testing
# Dry run: list how many assets would change without downloading
cd update
python update_repo.py --nofetch
123/2000 icons have changed
fetch disabled; not fetching 4567 assets

4.2 update_symbols.py — current Material Symbols

Downloads the current variable-font set. Drives off fonts.google.com/metadata/icons?incomplete=1&key=material_symbols and filters families to those containing "Symbols".

The combinatorial expansion is the core of this script. For each symbol icon and style it iterates:

  • optical sizes: 20, 24, 40, 48
  • fill: "", fill1
  • grade: gradN25, "", grad200
  • weight: wght100..wght700 (web/android only; iOS gets no weight variants)

producing the axis-encoded filenames you see under symbols/.

Destination pattern Asset
symbols/web/{name}/{set}/{name}{suffix}_{size}px.svg Web SVG
symbols/android/{name}/{set}/{name}{suffix}_{size}px.xml Android vector XML
symbols/ios/{name}/{set}/{name}{suffix}_symbol.svg iOS SVG (fill/grade only)
variablefont/{set}[FILL,GRAD,opsz,wght].css → .woff2 → .ttf Variable font

Fonts are requested via the Fonts CSS API with the full axis range (opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200), downloaded as WOFF2, then decompressed to TTF with fontTools.ttLib.woff2.

Command-line flags (absl):

Flag Default Effect
--fetch True Master fetch switch (--nofetch to dry-run)
--overwrite False Re-download and overwrite existing assets even if version unchanged
--icon_limit N 0 Process at most N icons

Note

update_symbols.py parallelizes downloads with joblib.Parallel(n_jobs=50) — fifty concurrent HTTP fetches — and sends a desktop-Chrome User-Agent header. Individual fetch failures are caught and printed rather than aborting the run, so a transient upstream error skips one asset rather than failing the whole job.

4.3 icons.py — font → codepoints extractor

A small utility that, given a built icon font, yields (icon_name, codepoint) pairs:

  1. Loads the font with fontTools.ttLib.TTFont.
  2. Builds a reverse cmap (glyph name → codepoint).
  3. Walks the GSUB ligature lookups (type 4, plus type 7 extensions wrapping type 4) to reconstruct each icon's ligature name from its component glyphs.
  4. Filters to codepoints in the Unicode Private Use Areas (where icon glyphs live) and emits the mapping.

This is what produces every .codepoints sidecar file in font/ and variablefont/.

Tip

icons.py is the most reusable piece of the toolchain — it is a general-purpose extractor for any Google-style ligature icon font, independent of this repo's directory conventions.


5. Asset Versioning Model

The repository has no global semantic version. Instead, every icon carries an independent integer version from upstream, recorded in update/current_versions.json.

{
    "action::home": 18,
    "action::delete": 12,
    "symbols::home": 245
}
  • Classic icons are keyed "{category}::{name}" (see _version_key in update_repo.py).
  • Symbols are keyed "symbols::{name}" (see _version_key in update_symbols.py).
  • On each run, an icon is skipped unless its upstream version is strictly greater than the recorded value (--overwrite bypasses this for symbols).
  • After fetching, the file is rewritten sorted and indented, so diffs are readable.

Important

current_versions.json is the entire incremental-build cache. Deleting or corrupting it forces a full re-download of all assets on the next run. It is committed to git precisely so CI runs stay incremental.

The README also tracks a human-facing "update history" (e.g. 4.0.0 in 2020 restructured the repo; 3.0.0 in 2016 changed the license to Apache 2.0). These are coarse repository milestones, not per-icon versions.


6. CI / Automation

.github/workflows/main.yml regenerates the repository on a schedule.

on:
  workflow_dispatch:        # manual trigger
  schedule:
  - cron: "0 2 * * 5"       # Fridays at 02:00 UTC
jobs:
  update-symbols:
    permissions:
      contents: write
    runs-on: ubuntu-latest
    steps:
    - name: free disk space        # reclaim space before a large checkout
    - uses: actions/checkout@v3
    - uses: actions/setup-python@v4 (python 3.11)
    - run: pip install -r update/requirements.txt
    - run: python update_symbols.py     # working-directory: ./update
    - name: commit                  # git add . ; commit if staged ; push origin master

Key operational facts:

  • Schedule: weekly, Fridays 02:00 UTC. Also manually dispatchable.
  • Disk pre-clean: the first step frees disk (swapoff, remove swapfile, apt clean, docker image prune) because the checked-out tree is multi-gigabyte and would otherwise exhaust the runner.
  • Scope: the workflow runs only update_symbols.py — i.e. it refreshes the current Material Symbols set. The classic update_repo.py is not wired into CI, consistent with the classic set being frozen.
  • Commit policy: git diff --staged --quiet || git ... commit. It commits only if there are staged changes, then pushes to master as the GitHub Actions bot.
  • Permissions: contents: write so the bot can push.

Warning

The committed identity uses an empty email (-c user.email='<>'). This works for CI commits but means automated commits have no associated GitHub user. If you fork and enable the workflow, set a real bot identity to keep commit attribution clean.

Caution

Because CI does git add . and pushes to master, anyone who forks and runs this workflow with contents: write will overwrite their fork's master with regenerated assets. Disable the schedule on forks you don't intend to keep current.


7. Requirements

Toolchain runtime

Requirement Value
Python 3.11 (per CI; typed-ast pin suggests 3.6–3.10 also work)
Network Outbound HTTPS to fonts.google.com and fonts.googleapis.com
Disk Several GB free (full asset tree)
OS Any (CI uses ubuntu-latest)

Python dependencies (update/requirements.txt)

Package Pinned Role
absl-py 1.2.0 CLI flags + app entry (app.run(main))
fonttools 4.14.0 Font parsing, WOFF2 decompression, ligature walking
requests 2.24.0 HTTP downloads
joblib 1.2.0 Parallel fetch (Parallel(n_jobs=50)) in symbols generator
brotli 1.0.9 WOFF2 (brotli) decompression support
regex 2020.7.14 Pattern support
click, black, pathspec, toml, typed-ast, attrs, appdirs, joblib, six, chardet, idna, urllib3, certifi various Transitive / tooling

Note

The dependency pins are old (2020-era). They work for the toolchain's narrow needs but are not security-patched. See §9 and §11.

Tip

Install into a virtualenv — .gitignore already excludes update/venv/ and update/__pycache__/:

cd update
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt

8. Operations

8.1 Cloning efficiently

A full clone is multi-gigabyte with millions of files. Choose the smallest checkout that meets your need.

# Shallow clone (latest snapshot, full tree)
git clone --depth 1 https://github.com/google/material-design-icons.git

# Sparse: only the current variable fonts
git clone --depth 1 --filter=blob:none --sparse \
  https://github.com/google/material-design-icons.git
cd material-design-icons
git sparse-checkout set variablefont symbols

Caution

On Windows/NTFS, checking out the full tree is dominated by per-file creation cost for the millions of small assets and can take many minutes even after the pack downloads. Prefer sparse checkouts on Windows, or take only font/ + variablefont/ if you just need fonts.

8.2 Running a regeneration manually

cd update
source venv/bin/activate

# Classic Material Icons (small test first):
python update_repo.py --icon_limit 5

# Current Material Symbols (small test first):
python update_symbols.py --icon_limit 5
5/5 icons have changed
Starting 1200 fetches
0/1200, estimating 90s left
...

Tip

Always validate with --icon_limit (and --nofetch for a pure dry run) before a full run. A full symbols run fans out to tens of thousands of fetches at n_jobs=50.

8.3 Forcing a full refresh

# Symbols: ignore the version ledger and overwrite everything
python update_symbols.py --overwrite

To force the classic generator to redo everything, remove the relevant keys from (or reset) current_versions.json — update_repo.py has no --overwrite, it gates purely on the version ledger.

8.4 Mirroring / vendoring for production

For a production app you typically want a pinned, minimal subset, not a live mirror:

  1. Take only the formats you ship (e.g. variablefont/*.woff2 + .codepoints).
  2. Self-host them; serve from your own origin to avoid third-party font CORS/availability dependence.
  3. Pin to a specific commit so icon shapes don't shift under you (the repo is regenerated weekly).

Important

If you rely on the hosted Google Fonts font instead, you inherit Google's availability and any upstream icon renames/removals. For long-lived UIs, vendor a pinned snapshot.

8.5 Logs & observability

The scripts print progress to stdout only (no log files, no metrics). The classic generator prints periodic ETA lines every ~5 s; the symbols generator prints "{i}/{total} complete" every 5000 fetches. In CI, this stdout is the only run record — check the Actions run log for a regeneration's outcome.


9. Security Analysis

Note

This is an asset repository with a tiny generator. The attack surface is small but worth stating precisely.

Area Assessment
Secrets None in the repo. CI uses the default GITHUB_TOKEN via contents: write; no custom secrets are referenced.
Network trust The generators fetch from fonts.google.com / fonts.googleapis.com over HTTPS and write_bytes the responses directly. They trust upstream content implicitly (no checksum/signature verification).
Input validation Minimal. update_symbols.py asserts the parsed font URL ends in .woff2; update_repo.py asserts .otf/.ttf. The metadata JSON is consumed after stripping a 5-char prefix with no schema validation.
ZIP handling update_repo.py calls ZipFile.extractall() on downloaded archives. extractall is vulnerable to path-traversal ("zip-slip") if an archive contains ../ entries. Acceptable only because the archives come from a trusted Google endpoint.
Code execution None of user input; scripts are run by maintainers/CI, not exposed as a service.
Supply chain Dependency pins are from 2020 and unpatched. black==19.10b0 is a pre-release dev tool. None are run against untrusted input, limiting impact.
Auth / authz N/A — no service. CI push is gated by GitHub repo permissions.

Hardening recommendations (for forks/mirrors):

  • Validate or sandbox extractall (e.g. reject entries with absolute paths or ..) before trusting any upstream archive.
  • Add response content-type/size sanity checks before write_bytes.
  • Refresh dependency pins (see §11).
  • Set a real CI bot identity rather than the empty email.

Caution

If you point these scripts at any host other than Google's (e.g. a proxy/mirror), the implicit-trust model becomes a real risk: arbitrary downloaded bytes are written to disk and ZIPs are exploded without traversal checks. Only run against the trusted upstream.


10. Code Quality & Patterns

Strengths:

  • Declarative asset definitions. Both generators express the entire output layout as a tuple of Asset(src_pattern, dest_pattern) records, expanded by .format(**args). The directory conventions are fully described by these patterns — readable and self-documenting.
  • Incremental by design. The version-ledger gate keeps weekly runs cheap.
  • Clean separation. icons.py (font introspection) is decoupled from the fetch/layout logic.
  • Honest naming. Both files' docstrings call themselves "Quick and dirty utility" — an accurate self-assessment.

Weaknesses / debt:

  • Duplication. update_repo.py and update_symbols.py share large amounts of near-identical scaffolding (Asset/Fetch/Icon NamedTuples, _pattern_args, _fetch_fonts, _create_fetch, current_versions handling) with small divergences. A shared module would reduce drift.
  • Deeply nested combinatorial loop. The symbols main() nests size → fill → grade → weight loops inline; the axis lists are hard-coded magic values.
  • Silent failure in symbols fetch. _do_fetch swallows exceptions and prints them; a partial run can leave gaps without a non-zero exit, so CI won't flag missing assets.
  • No tests. There is no test suite for the toolchain.
  • No checksum verification of downloaded assets.

Note

The AST-level extraction over this repo finds only ~4 functions and 6 NamedTuple classes total — an accurate reflection that the codebase is small. The five-million-file scale is entirely generated output, not logic.


11. Potential Improvements

  • [ ] De-duplicate the two generators. Extract shared Asset/Fetch/Icon, _pattern_args, _fetch_fonts, and version-ledger logic into a common module imported by both.
  • [ ] Refresh dependencies. Bump requests, urllib3, certifi, fonttools, and drop the pinned black==19.10b0 pre-release; the 2020-era pins carry known CVEs even if not exploitable here.
  • [ ] Harden ZIP extraction. Replace bare extractall() with a guarded extraction that rejects path-traversal entries.
  • [ ] Fail loudly. Make update_symbols.py track fetch failures and exit non-zero (or at least summarize failures) so CI surfaces partial runs.
  • [ ] Wire the classic generator (or document why not). CI only runs update_symbols.py; if the classic set is permanently frozen, state that in the workflow comment so it's not mistaken for an omission.
  • [ ] Add a smoke test. A tiny test (e.g. icons.py against a bundled sample font) would guard the most reusable component.
  • [ ] Configurable concurrency. Hoist n_jobs=50 and the axis lists into flags/constants instead of magic numbers.
  • [ ] Checksum/size sanity checks before writing downloaded bytes.

Note

These are observations about the toolchain. Most consumers never touch it — for them, the operational improvement that matters most is pinning a snapshot rather than tracking weekly-regenerated master.


12. Quick Reference

Toolchain commands

Task Command (run from update/)
Dry-run classic python update_repo.py --nofetch
Test classic (5 icons) python update_repo.py --icon_limit 5
Full classic refresh python update_repo.py
Skip existing classic python update_repo.py --skip_existing
Dry-run symbols python update_symbols.py --nofetch
Test symbols (5 icons) python update_symbols.py --icon_limit 5
Force-overwrite symbols python update_symbols.py --overwrite

Upstream endpoints

Endpoint Used by
fonts.google.com/metadata/icons?incomplete=1 update_repo.py (classic metadata)
fonts.google.com/metadata/icons?incomplete=1&key=material_symbols update_symbols.py (symbols metadata)
{host}/s/i/... per-asset SVG/zip/xml downloads
fonts.googleapis.com/css2?family=... font CSS → binary resolution

Generated output directories

Directory Set Format
src/ Classic SVG (24px)
png/ Classic PNG (18–48dp, 1x/2x/3x)
android/ Classic Vector XML + density PNG
ios/ Classic .xcassets imagesets
font/ Classic .otf/.ttf + .codepoints
`symbols/web android ios/`
variablefont/ Current .ttf/.woff2 + .codepoints

CI summary

Property Value
File .github/workflows/main.yml
Trigger workflow_dispatch + cron 0 2 * * 5 (Fri 02:00 UTC)
Runs update_symbols.py only
Python 3.11
Output Commits regenerated symbols assets to master

Facts & negative knowledge

  • There is no application server, API, or database in this repository.
  • There is no global version number; versioning is per-icon via current_versions.json.
  • CI does not run update_repo.py (classic generator); only update_symbols.py.
  • Material Symbols iOS assets have no weight (wght) variants — only fill/grade.
  • Material Icons (classic) has no variable axes and no two-tone in the Symbols set.
  • Google does not accept pull requests for icon files.