Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
edb6a2d
add script and spec file
wpkelso Feb 6, 2025
e1961f1
Output list of files in csv file
wpkelso Feb 7, 2025
582d542
Add help usage message
wpkelso Feb 7, 2025
6968c10
Merge branch 'main' into audit-script
wpkelso Feb 7, 2025
679b88e
correct noted entries
wpkelso Feb 12, 2025
aef1a29
Exclude symbolic icons from report
wpkelso Feb 12, 2025
d016dad
Symbolic is common practice enough that showing it in out-of-spec isn…
wpkelso Feb 12, 2025
75c1a09
Merge branch 'main' into audit-script
wpkelso Feb 14, 2025
b7a7f57
add optional support for pulling the spec off the web
wpkelso Feb 24, 2025
ac84436
Clean up remote pull and use argparse like a civilized script
wpkelso Feb 24, 2025
94ffd92
Merge branch 'main' into audit-script
wpkelso Apr 4, 2025
48e0075
Merge branch 'main' into audit-script
wpkelso May 29, 2025
e2d3e8f
Merge branch 'main' into audit-script
wpkelso Jun 12, 2025
18aa636
Merge branch 'main' into audit-script
wpkelso Jul 11, 2025
7f7fbb3
Merge branch 'main' into audit-script
zeebok Sep 18, 2025
12b0688
Merge branch 'main' into audit-script
danirabbit Feb 17, 2026
da94c8e
document script and add .venv to gitignore
wpkelso Feb 17, 2026
5ab6511
mention python3 system package reqs
wpkelso Feb 17, 2026
317f3e2
set a user agent for requests
wpkelso Feb 17, 2026
13df2f5
rm debug print
wpkelso Feb 17, 2026
e17736b
pythonify and restrict considered files to svg
wpkelso Feb 20, 2026
ebab803
specf called incorrectly
wpkelso Feb 20, 2026
76b90cf
correct typos
wpkelso Feb 20, 2026
eb31477
add option to specify which list to report
wpkelso Feb 20, 2026
faa73e2
remove flag icon code
wpkelso Feb 25, 2026
74fc4cf
Generalize and don't pull from web if file is specified
wpkelso Feb 26, 2026
5e78392
use more standard convetion for output file name
wpkelso Mar 2, 2026
7251c08
flag-gate file output
wpkelso Mar 2, 2026
7b1b006
Flag-gate symbolics and add color to missing entries
wpkelso Mar 2, 2026
2fa1485
Add note on limitations with build system symlinks
wpkelso Mar 2, 2026
d3ae6af
symbolics aren't part of the spec at all, they're gtk platform specific
wpkelso Mar 6, 2026
0c0b742
Merge branch 'main' into audit-script
wpkelso Mar 11, 2026
ba05e3c
Merge branch 'main' into audit-script
wpkelso Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*~
build/
.venv/
17 changes: 17 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,22 @@ Since this set is designed specifically for elementary OS, pull requests to add

Use of icon names in line with the [FreeDesktop.Org Icon Naming Specification](http://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html) is encouraged.

To monitor coverage, an auditing python script is included under `/scripts`. While this
script can be used without any extra dependencies, setting up a virtual
environment and installing `requests` and `BeautifulSoup4` enables functionality
that ensures all resources pulled are as current as possible. To do this:

1. If not already installed, run `apt install python3 python3.12-venv`
2. From the repository root, run `python3 -m venv .venv`. This sets up a python
virtual environment in a hidden folder, scoped to this repository.
3. To activate the venv, run `source .venv/bin/activate`.
4. To install `requests` and `BeautifulSoup4`, run `python3 -m pip install requests
BeautifulSoup4`
5. Run the script with `python3 scripts/audit.py <path to theme folder>`

Note that for accurate results, the fully built theme directory has to be
targeted, not the development files. Many symlinks are created by the build
system, so will not be picked up if this script is run on the development files.

## Third-Party Brand Preservation
elementary icons do not attempt to supply icons for third-party apps. Pull requests to add icons or symbolic links that would overwrite the branding of third-party apps will be rejected.
275 changes: 275 additions & 0 deletions scripts/audit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
#!/usr/bin/env python3
Comment thread
wpkelso marked this conversation as resolved.

# # Fd.o Icon Name Specification Audit

# ## Description:
#
# Audit a Freedesktop.org icon specification-compatible theme against the icon
# naming specification, returning a report of what icons were included that are
# in-spec, what icons were included that are out-of-spec, and what icons were
# missing that are included in the spec.

# ## Dependencies:
#
# - requests (optional)
# - bs4 (optional)

import os, csv, argparse

parser = argparse.ArgumentParser(
prog = "audit",
description = "Audit a FreeDesktop.org compatible icon set against a specification"
)
parser.add_argument(
"theme_path",
)
parser.add_argument(
"-s",
"--specification",
dest="specf"
)
parser.add_argument(
"-v",
"--verbose",
action="store_true"
)
parser.add_argument(
"-o",
"--output",
dest="fout",
)
parser.add_argument(
"-d",
"--display-results",
dest="to_display",
choices=["all", "found", "missing", "extra"],
default="all"
)
args = parser.parse_args()

# make hrules pretty
try:
view_width = os.get_terminal_size().columns
except:
view_width = 80

# Check for optional dependencies to enable drawing the spec from the web.
# Otherwise fall back to included file
try:
found_deps = False
import requests
from bs4 import BeautifulSoup
except:
print("Couldn't find bs4 and requests dependencies, falling back to using spec file…")
else:
if args.specf is None:
print("Found bs4 and requests dependencies, pulling spec from the web…")
found_deps = True

def get_soup(url: str) -> BeautifulSoup:
page = session.get(url)
soup = BeautifulSoup(page.text, "html.parser")
return soup

# This is kinda not great but it works
def parse_soup(soup: BeautifulSoup) -> list:
result = []

tables = soup.find_all("table")
#remove the context description table, we don't care about it here
tables.pop(0)
for table in tables:
rows = table.find_all("tr")
for row in rows:
text = row.contents
name = text[0].text.strip().replace("\n", "")
# skip the header row
if name == "Name": continue
result.append(name)

return result

def pad_list(iterable: list, length: int) -> list:
extension_length = max(length - len(iterable), 0)
if args.verbose: print(f"Extending list by {extension_length} elements")
for _ in range(extension_length):
iterable.append(None)

return iterable

# Place this in a top level context so we don't have to pass it around
ignore_list = []

def populate_from_dir(dir: os.DirEntry) -> set:
# This function is called recursively to populate a list
tree = []

for entry in os.scandir(dir.path):
if entry.name in ignore_list:
continue

if entry.is_dir():
if args.verbose: print(f"Found dir at {entry.path}, going deeper")
# A spoonful of recursion to help the medicine go down
tree.extend(populate_from_dir(entry))
elif entry.name.endswith(".svg"):
# We only check svg because that's the only type we use in elementary icons
if args.verbose: print(f"Adding file {entry.path}, named {entry.name} to SubTree")
tree.append(entry.name.removesuffix(".svg"))

if args.verbose: print(f"The SubTree for Dir {dir.name} is {tree}")
return list(set(tree))

# Load a text stream of the spec into a dictionary, where KEY=icon_name &
# VALUE=a bool denoting whether the file was found in the file tree
# Iterate over the list, checking to see if the specified file can be found.
specification = {}

if found_deps:
url = "https://specifications.freedesktop.org/icon-naming-spec/latest/"
headers = {
'User-Agent': 'fdo-icon-name-spec-audit-script/1.0.0',
}

session = requests.Session()
session.headers.update(headers)

soup = get_soup(url)
spec_list = parse_soup(soup)
specification = specification.fromkeys(spec_list, False)
else:
with open(args.specf, "r") as file:
print(f"Loading specification in file: {args.specf}…")
for line in file:
if line.startswith(("//", " ", "\n")):
continue
specification.setdefault(line.removesuffix("\n"), False)

print("Successfully loaded specification!")

# Change the working dir to our target dir, to make it easier to traverse the
# tree. Try to find the file `.auditignore` in the dir root and load it into
# a list. Anything in this list doesn't exist as far as this script is
# concerned.
try:
target_dir = args.theme_path
except:
print("Please specify a directory")
exit()
else:
# Save the cwd so we can switch back later
script_dir = os.getcwd()
dir = os.fspath(target_dir)
os.chdir(dir)

try:
with open(".auditignore", "r") as file:
Comment thread
wpkelso marked this conversation as resolved.
print("Found .auditignore file!")
for line in file:
ignore_list.append(line.strip())
except:
print("No .auditignore file found in root")
else:
print("Loaded .auditignore file!")
if args.verbose: print(f"Values contained: {ignore_list}")

print('-' * view_width)

# Generate the contents of the relevant directory tree
contents = []

for entry in os.scandir():
if entry.name not in ignore_list:
if entry.is_dir():
if args.verbose: print(f"Found dir at {entry.path}")
sub_dir_tree = populate_from_dir(entry)
contents.extend(sub_dir_tree)
else:
if entry.name.endswith(".svg") and not
entry.name.endswith("-symbolic.svg"):
if args.verbose: print(f"Adding file {entry.path} to tree")
contents.append(entry.name.removesuffix(".svg"))

# remove duplicate names from the list
contents = list(set(contents))

if args.verbose:
print(f"The full dirTree is {contents}")
print('-' * view_width)

# Traverse the dir tree, checking whether the found file is in the spec list.
# This is so we don't have to traverse the entire dir tree of the set, just the
# specification list which is likely to be much smaller.

for entry in specification.keys():
if entry in contents:
print(f"Found {entry}")
specification |= {entry: True}
else:
print(f"\033[31m[!!] {entry} is missing!\033[0m")


# Calculate percent spec coverage
total_entries = len(specification.keys())
existant_color_entries = list(specification.values()).count(True)
color_results = list(specification.values())

print(f"{existant_color_entries / total_entries * 100:.2f}% coverage of specification")

print('-' * view_width)

if args.fout is not None:
os.chdir(script_dir)

# Write report file with Found, Missing, and Extra information
spec_list = list(specification.keys())
found_entries = []
missing_entries = []
extra_entries = []

# We only look at the color entry results, as symbolic is not officially in the spec
for i, result in enumerate(color_results):
# If the result is true, then it was found
# If the result is false, then it is missing
name = spec_list[i]
if result:
if args.verbose: print(f"Adding {name} to Found…")
found_entries.append(name)
else:
if args.verbose: print(f"Adding {name} to Missing…")
missing_entries.append(name)

# Now we want to go through all of the entries we found in the initial contents and
# point out those that aren't in the spec
for entry in contents:
if entry not in spec_list and not entry.endswith("-symbolic"):
if args.verbose: print(f"Adding {entry} to Extra")
extra_entries.append(entry)

# Pad all lists to be the same length, then zip all three lists together
length = max(len(found_entries), len(missing_entries), len(extra_entries))

with open(args.fout, 'w', newline='') as file:
fwriter = csv.writer(file)
match args.to_display:
case "found":
fwriter.writerow(["Found"])
fwriter.writerows(map(lambda x: [x], found_entries))
case "missing":
fwriter.writerow(["Missing"])
fwriter.writerows(map(lambda x: [x], missing_entries))
case "extra":
fwriter.writerow(["Extra"])
fwriter.writerows(map(lambda x: [x], extra_entries))
case _:
fwriter.writerow(["Found", "Missing", "Extra"])

found_entries = pad_list(found_entries, length)
missing_entries = pad_list(missing_entries, length)
extra_entries = pad_list(extra_entries, length)
zipped_lists = [(a, b, c) for a, b, c in zip(found_entries,
missing_entries, extra_entries)]
fwriter.writerows(zipped_lists)

print(f"Report written to {os.getcwd()}/{args.fout}")

Loading