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
- Executive Summary
- System Architecture
- Repository Structure
- The Update Toolchain
- Asset Versioning Model
- CI / Automation
- Requirements
- Operations
- Security Analysis
- Code Quality & Patterns
- Potential Improvements
- 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:
- The toolchain fetches the icon metadata index from
fonts.google.com/metadata/icons(a JSON document with a 5-character prefix stripped before parsing). - For each icon whose upstream
versionexceeds the version recorded incurrent_versions.json, it computes a set of fetch URLs from templated patterns and downloads each asset. - 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.
- For each downloaded font,
icons.pyenumerates ligatures to emit a.codepointssidecar mapping icon name → Unicode codepoint. current_versions.jsonis 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 onupdate/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.pyparallelizes downloads withjoblib.Parallel(n_jobs=50)— fifty concurrent HTTP fetches — and sends a desktop-ChromeUser-Agentheader. 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:
- Loads the font with
fontTools.ttLib.TTFont. - Builds a reverse cmap (glyph name → codepoint).
- Walks the
GSUBligature lookups (type 4, plus type 7 extensions wrapping type 4) to reconstruct each icon's ligature name from its component glyphs. - 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.pyis 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_keyinupdate_repo.py). - Symbols are keyed
"symbols::{name}"(see_version_keyinupdate_symbols.py). - On each run, an icon is skipped unless its upstream
versionis strictly greater than the recorded value (--overwritebypasses this for symbols). - After fetching, the file is rewritten sorted and indented, so diffs are readable.
Important
current_versions.jsonis 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 classicupdate_repo.pyis 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 tomasteras the GitHub Actions bot. - Permissions:
contents: writeso 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 tomaster, anyone who forks and runs this workflow withcontents: writewill overwrite their fork'smasterwith 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 —
.gitignorealready excludesupdate/venv/andupdate/__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--nofetchfor a pure dry run) before a full run. A full symbols run fans out to tens of thousands of fetches atn_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:
- Take only the formats you ship (e.g.
variablefont/*.woff2+.codepoints). - Self-host them; serve from your own origin to avoid third-party font CORS/availability dependence.
- 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.pyandupdate_symbols.pyshare large amounts of near-identical scaffolding (Asset/Fetch/IconNamedTuples,_pattern_args,_fetch_fonts,_create_fetch,current_versionshandling) 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_fetchswallows 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 pinnedblack==19.10b0pre-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.pytrack 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.pyagainst a bundled sample font) would guard the most reusable component. - [ ] Configurable concurrency. Hoist
n_jobs=50and 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); onlyupdate_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.