Project 2: Library Dependency Visualizer
Build a CLI tool that parses binaries, resolves shared library dependencies the same way the dynamic loader does, and outputs a deterministic dependency graph.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 3: Advanced |
| Time Estimate | 1-2 weeks |
| Main Programming Language | C (Alternatives: Python, Rust) |
| Alternative Programming Languages | Python (parsing), Rust |
| Coolness Level | Level 3: Genuinely Clever |
| Business Potential | Level 3: Service & Support Model |
| Prerequisites | ELF basics, filesystem traversal, graph fundamentals |
| Key Topics | ELF .dynamic, DT_NEEDED, loader search paths, graph traversal |
1. Learning Objectives
By completing this project, you will:
- Parse ELF (or Mach-O/PE) metadata to extract
DT_NEEDEDdependencies. - Reimplement the runtime loader’s search order to resolve library paths.
- Build a dependency graph with cycle detection and deterministic output.
- Generate DOT/SVG/ASCII graphs for complex dependency trees.
- Explain loader failures by showing exactly which dependency could not be resolved.
2. All Theory Needed (Per-Concept Breakdown)
2.1 ELF Dynamic Section and DT_NEEDED
Fundamentals
The ELF dynamic section is the loader’s roadmap. It contains tagged entries that tell the runtime loader which shared libraries must be loaded (DT_NEEDED), the shared object’s identity (DT_SONAME), and where to find relocation tables. For a dependency visualizer, the most important tag is DT_NEEDED, which is a list of library names like libcrypto.so.3. The loader uses these names plus its search paths to locate actual files. If you can parse the dynamic section and read the string table that backs it, you can reconstruct the full dependency list for any binary.
Deep Dive into the concept
ELF files have two major tables: the section header table and the program header table. The dynamic section is a section called .dynamic, but it is usually mapped into memory because there is also a PT_DYNAMIC program header pointing to it. The loader ignores most sections but uses the program header to find .dynamic. Each dynamic entry is a Elf64_Dyn structure with a tag and a value. Tags like DT_STRTAB and DT_STRSZ point to the dynamic string table, which stores strings like libc.so.6 or libpthread.so.0. DT_NEEDED entries are indexes into that string table. Therefore, to parse them correctly, you must read DT_STRTAB and then resolve offsets for each DT_NEEDED entry.
The dynamic section also contains DT_RPATH or DT_RUNPATH, which are strings in the same dynamic string table. These are not dependencies themselves, but they influence search paths. For a visualizer, you should capture them because they explain why a particular library was resolved from a non-standard path. Another useful tag is DT_SONAME for libraries, which defines the ABI identity of the library. When you resolve a dependency by file path, it is good practice to record both the file path and its DT_SONAME.
Remember that the dynamic section is different from the symbol table. A dependency visualizer does not need to parse .dynsym to list dependencies, but if you want to explain missing symbols or unresolved relocations later, .dynsym becomes relevant. For this project, focus on dependencies and their paths. Use readelf -d as a reference to validate your parser output.
Even within ELF, variations exist: 32-bit vs 64-bit, endianness, and different machine architectures. Your tool should either explicitly support only the platform it runs on (simpler) or include parsing logic that checks e_ident fields and rejects unsupported files with a clear error. Deterministic output means your graph should always present nodes in a stable order (e.g., sorted by name), regardless of filesystem traversal order.
Finally, recognize that executables may use DT_NEEDED indirectly: if app depends on libA, which depends on libB, you must recursively parse each library. You need to avoid cycles and duplicates; the DT_NEEDED list alone does not encode full paths. That is where loader search rules come in.
How this fits in this project
Your parser will extract DT_NEEDED for each binary and library and feed these into the graph builder. The dynamic section is the single source of truth for dependency names.
Definitions & key terms
.dynamic-> Section containing runtime linking metadata.DT_NEEDED-> List of library names required by the binary.DT_STRTAB-> Pointer to dynamic string table.DT_SONAME-> ABI identity string for a shared library.PT_DYNAMIC-> Program header pointing to the dynamic section.
Mental model diagram (ASCII)
ELF file
+-------------------+
| ELF header |
+-------------------+
| Program headers | -> PT_DYNAMIC
+-------------------+
| .dynamic section | -> DT_NEEDED -> string table
+-------------------+
How it works (step-by-step, with invariants and failure modes)
- Read ELF header and validate magic (
0x7f 'E' 'L' 'F'). - Locate program headers and find
PT_DYNAMIC. - Parse dynamic entries and locate
DT_STRTAB. - Read each
DT_NEEDEDentry and resolve it via the string table. - Use the loader search algorithm to map each name to a path.
Invariants: ELF headers must be valid, string table offsets must be in range. Failure modes: corrupted ELF, missing DT_STRTAB, or unknown endianness.
Minimal concrete example
$ readelf -d /usr/bin/ssh | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libcrypto.so.3]
0x0000000000000001 (NEEDED) Shared library: [libz.so.1]
Common misconceptions
- “
lddoutput is the same asDT_NEEDED.” ->lddalready resolves paths using loader rules. - “Sections map directly into memory.” -> The loader uses program headers, not sections.
Check-your-understanding questions
- Why does
DT_NEEDEDstore library names instead of full paths? - What information do you need from
DT_STRTAB? - Why should a tool verify ELF endianness and class?
Check-your-understanding answers
- The loader resolves names using search paths that can vary by environment.
- It provides the string table to convert offsets into actual names.
- Parsing with the wrong endianness or class yields invalid offsets and crashes.
Real-world applications
- Diagnosing startup failures for large binaries.
- Auditing dependencies for security or packaging.
Where you’ll apply it
- In this project: see Section 3.2 Functional Requirements and Section 4.4 Algorithm Overview.
- Also used in: P06-minimal-dynamic-linker.
References
- System V ABI (gABI) - ELF specification.
- “Linkers and Loaders” (Levine), Ch. 10.
Key insights
DT_NEEDED is the loader’s dependency list; parse it correctly and the rest becomes a path resolution problem.
Summary The dynamic section encodes dependency names and essential loader metadata. Your tool must read it faithfully and deterministically to build accurate graphs.
Homework/Exercises to practice the concept
- Use
readelf -don a system binary and list its dependencies. - Write a script that reads
DT_NEEDEDentries and prints them sorted. - Compare
.dynamicoutput of an executable and a shared library.
Solutions to the homework/exercises
readelf -d /bin/ls | grep NEEDED.- Use
pyelftoolsor a small C parser to readDT_NEEDED. readelf -d /lib/x86_64-linux-gnu/libc.so.6.
2.2 Loader Search Paths (RPATH, RUNPATH, LD_LIBRARY_PATH, ldconfig)
Fundamentals
The dynamic loader locates libraries by searching a set of paths in a defined order. The default is system directories (e.g., /lib, /usr/lib), but binaries can embed RPATH or RUNPATH entries, and users can override paths via LD_LIBRARY_PATH. On Linux, ldconfig maintains a cache of library locations. If your visualizer resolves dependencies incorrectly, your graph will not match reality. Therefore you must implement the search order exactly, or explicitly document which parts you skip.
Deep Dive into the concept The loader’s search algorithm is nuanced. For an executable, the loader uses (roughly) this order:
LD_LIBRARY_PATH(unlessDT_RUNPATHis present and secure-exec disables it)DT_RPATH(ifDT_RUNPATHis absent)DT_RUNPATHldconfigcache (/etc/ld.so.cache)- Default directories (
/lib,/usr/lib, and multilib variants)
For shared libraries, the rules differ slightly because the library’s own RPATH/RUNPATH may apply to its dependencies. Also, DT_RPATH is deprecated in favor of DT_RUNPATH, and the presence of RUNPATH changes how LD_LIBRARY_PATH is applied. The loader also respects environment variables like LD_PRELOAD and can be forced to debug via LD_DEBUG=libs.
To emulate this, your tool should model the search order and allow the user to pass in an environment configuration (e.g., a fake LD_LIBRARY_PATH). For determinism, you should not depend on current environment unless explicitly requested; instead, allow flags like --ld-path or --no-env. When resolving a library name, you should check each candidate directory in order. If multiple paths contain the same library, the first match wins. This detail is important because it can explain subtle differences between machines.
On macOS and Windows, the search order differs (install names, @rpath, PATH, DYLD_LIBRARY_PATH). If you plan cross-platform support, you should abstract the resolver so each platform has its own path resolution strategy. For this project, implementing Linux accurately is sufficient, but document the limitations.
Finally, remember that ldd is just a tool that uses the loader. It is not an oracle for path order if environment variables are set. That is why your tool must be explicit about which environment it emulates.
How this fits in this project Your resolver will implement the search algorithm and feed fully resolved paths into the graph. The visualizer’s credibility depends on path accuracy.
Definitions & key terms
- RPATH -> Embedded search path used when RUNPATH is absent.
- RUNPATH -> Embedded search path with different precedence rules.
- LD_LIBRARY_PATH -> Environment override for library search.
- ldconfig cache -> System cache of library locations.
Mental model diagram (ASCII)
Resolve "libfoo.so" by checking:
1) LD_LIBRARY_PATH
2) RPATH (if no RUNPATH)
3) RUNPATH
4) ld.so.cache
5) /lib, /usr/lib
How it works (step-by-step, with invariants and failure modes)
- Read binary’s
DT_RPATHandDT_RUNPATH. - Build an ordered list of search directories.
- For each
DT_NEEDEDname, check each directory for a matching file. - If found, record path; if not, mark as unresolved.
Invariants: search order must be deterministic. Failure modes: missing cache, environment differences, or missing permissions.
Minimal concrete example
$ LD_LIBRARY_PATH=/tmp/libs ./libviz ./app
# libviz resolves first match in /tmp/libs before system dirs
Common misconceptions
- “RUNPATH is the same as RPATH.” -> RUNPATH changes precedence rules.
- “
LD_LIBRARY_PATHalways applies.” -> Secure execution can disable it.
Check-your-understanding questions
- Why does RUNPATH change the effect of LD_LIBRARY_PATH?
- What happens if two directories contain the same SONAME?
- Why is
ldconfigcache important?
Check-your-understanding answers
- With RUNPATH, LD_LIBRARY_PATH is searched first, whereas with RPATH it may not be.
- The first directory in search order wins, which can change the resolved library.
- It speeds up lookup and includes system-defined library locations.
Real-world applications
- Debugging “works on my machine” dependency issues.
- Packaging software with custom library paths.
Where you’ll apply it
- In this project: see Section 3.5 Data Formats and Section 5.10 Phase 2.
- Also used in: P03-ld-preload-interceptor for symbol resolution precedence.
References
man ld.so(loader search order).- “The Linux Programming Interface” (Kerrisk), Ch. 41.
Key insights Library resolution is deterministic but environment-sensitive; your tool must model both to be trustworthy.
Summary Implementing the loader’s search order accurately is the difference between a pretty graph and a correct one.
Homework/Exercises to practice the concept
- Create two directories with different versions of the same library and observe resolution order.
- Use
readelf -dto compare RPATH and RUNPATH in two binaries. - Inspect
/etc/ld.so.cachewithldconfig -p.
Solutions to the homework/exercises
- Place
libfoo.soin/tmp/aand/tmp/b, setLD_LIBRARY_PATH=/tmp/a:/tmp/band observe the match. - Build one binary with
-Wl,-rpathand another with-Wl,-runpath. ldconfig -p | grep libfoo.
2.3 Graph Traversal, Cycles, and Deterministic Output
Fundamentals
A dependency graph is a directed graph where nodes are binaries/libraries and edges represent DT_NEEDED dependencies. Graph traversal is required to walk transitive dependencies. Cycles can exist (e.g., libA depends on libB and libB depends on libA), so your traversal must track visited nodes to avoid infinite recursion. Determinism is important for testing: the same input must always produce the same graph output, even if file system iteration order changes.
Deep Dive into the concept
Graph traversal for dependency resolution is similar to DFS or BFS. The simplest approach is DFS: start at the root binary, visit each dependency, and recursively visit their dependencies. To avoid cycles, maintain a visited set keyed by canonical path or SONAME. The choice of key matters. If you use raw file paths, you might treat two paths to the same file as different nodes. A robust solution is to resolve to real paths (realpath) and store those, along with SONAME metadata to handle symlinks.
Deterministic output requires that you consistently order both nodes and edges. That means you must sort dependency lists before traversing, and you must generate output in a stable order. If you parse DT_NEEDED entries, they are typically already ordered as written by the linker, but filesystem search can yield different results on different machines. Sorting by resolved path or SONAME ensures your output is stable. If you output DOT, you should also sort nodes and edges when printing.
Cycle representation should be explicit. If you detect a cycle, you can still output the edge but mark it as a cycle edge. This makes the graph informative rather than misleading. Additionally, when a dependency cannot be resolved, you should include a placeholder node like libfoo.so (missing) and still draw the edge. This helps users see what is missing.
Complexity matters: large binaries can have many dependencies. Use a queue/stack for traversal and store adjacency lists. Complexity is O(V + E) where V is the number of libraries and E is the number of dependency edges. For deterministic output, you might pay a cost for sorting; that is O(E log E) overall but acceptable for typical sizes.
How this fits in this project The graph traversal algorithm is the core of your tool. It determines which libraries are visited, avoids cycles, and produces the output formats.
Definitions & key terms
- Directed graph -> Edges have direction (A depends on B).
- DFS/BFS -> Graph traversal strategies.
- Cycle -> A path that returns to a previously visited node.
- Deterministic output -> Output order is stable across runs.
Mental model diagram (ASCII)
app
|-> libA
| |-> libC
|-> libB
|-> libC (shared node)
How it works (step-by-step, with invariants and failure modes)
- Add root binary as the first node.
- Extract dependencies and sort them.
- For each dependency, resolve path and add edge.
- If node not visited, traverse recursively.
- If node already visited, mark a cycle edge.
Invariants: each node is visited once, edges preserve correct direction. Failure modes: cycles causing recursion overflow or non-deterministic ordering.
Minimal concrete example
Nodes: app, libA, libB, libC
Edges: app->libA, app->libB, libA->libC, libB->libC
Common misconceptions
- “
lddshows a tree.” -> It is a graph, not a tree. - “Cycles cannot exist.” -> Cycles are possible in shared libraries.
Check-your-understanding questions
- Why is a visited set required in dependency traversal?
- How does deterministic output help testing?
- When would you choose DFS over BFS here?
Check-your-understanding answers
- To avoid infinite recursion in cycles.
- It allows golden files and stable diffs.
- DFS is simpler to implement and adequate for dependency graphs.
Real-world applications
- Build systems that need to calculate link order.
- Security scanning for transitive dependencies.
Where you’ll apply it
- In this project: see Section 4.4 Algorithm Overview and Section 6.2 Critical Test Cases.
- Also used in: P06-minimal-dynamic-linker for dependency resolution.
References
- “Algorithms” (Sedgewick/Wayne), graph traversal chapters.
- Graphviz DOT format reference.
Key insights Treat dependency graphs as graphs, not trees, and enforce determinism to make your tool testable.
Summary Dependency visualization is a graph problem. The correctness of your tool depends on robust traversal, cycle handling, and stable output ordering.
Homework/Exercises to practice the concept
- Build a graph with a known cycle and ensure your traversal terminates.
- Output DOT and render it with Graphviz.
- Implement deterministic sorting of edges and verify identical output across runs.
Solutions to the homework/exercises
- Use a visited set keyed by node ID.
dot -Tsvg deps.dot -o deps.svg.- Sort edges by
(from, to)before printing.
3. Project Specification
3.1 What You Will Build
A CLI tool named libviz that:
- Reads an executable or shared library file.
- Recursively resolves
DT_NEEDEDdependencies using loader search rules. - Emits a dependency graph in DOT, SVG, and ASCII formats.
- Produces deterministic output given fixed inputs and search paths.
3.2 Functional Requirements
- ELF parser: Extract
DT_NEEDED,DT_RPATH,DT_RUNPATH. - Resolver: Implement loader search order with optional
--ld-pathoverride. - Graph builder: Build directed graph with cycle detection.
- Output formats: DOT, SVG (via Graphviz), ASCII.
- Error reporting: Missing dependency nodes and clear diagnostics.
3.3 Non-Functional Requirements
- Determinism: Same input yields same output.
- Portability: Linux focus; reject unsupported formats gracefully.
- Usability: Clear flags and helpful error messages.
3.4 Example Usage / Output
$ ./libviz /usr/bin/ssh --output deps.dot --format dot --no-env
[scan] /usr/bin/ssh
[deps] libcrypto.so.3 -> /lib/x86_64-linux-gnu/libcrypto.so.3
[deps] libz.so.1 -> /lib/x86_64-linux-gnu/libz.so.1
[graph] wrote deps.dot (27 nodes, 42 edges)
3.5 Data Formats / Schemas / Protocols
DOT output
digraph deps {
"app" -> "libcrypto.so.3";
"app" -> "libz.so.1";
}
ASCII output (minimal)
app
|-- libcrypto.so.3
|-- libz.so.1
3.6 Edge Cases
- Binary with missing
PT_DYNAMIC. - Dependencies resolved via
RUNPATHonly. - Cycles between libraries.
- Libraries missing on disk.
3.7 Real World Outcome
3.7.1 How to Run (Copy/Paste)
./libviz ./app --format dot --output deps.dot --ld-path /custom/libs --no-env
3.7.2 Golden Path Demo (Deterministic)
- Input:
tests/fixtures/app - Search paths:
tests/fixtures/libs - Output:
tests/golden/app_deps.dot
3.7.3 CLI Transcript (Success + Failure)
$ ./libviz tests/fixtures/app --format dot --output deps.dot --ld-path tests/fixtures/libs --no-env
[scan] tests/fixtures/app
[deps] libfoo.so.1 -> tests/fixtures/libs/libfoo.so.1
[deps] libc.so.6 -> /lib/x86_64-linux-gnu/libc.so.6
[graph] wrote deps.dot (3 nodes, 2 edges)
[exit] code=0
$ ./libviz tests/fixtures/app --format dot --output deps.dot --ld-path /tmp/empty --no-env
[scan] tests/fixtures/app
[error] unresolved dependency: libfoo.so.1
[graph] wrote deps.dot (2 nodes, 1 edges, 1 missing)
[exit] code=5
3.7.4 If CLI: Exit Codes
0: success4: invalid ELF or unsupported format5: unresolved dependency6: Graphviz rendering failed
4. Solution Architecture
4.1 High-Level Design
+------------------+
| libviz (CLI) |
| - ELF parser |
| - path resolver |
| - graph builder |
| - output writer |
+---------+--------+
|
v
DOT / SVG / ASCII output
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| ELF parser | Extract DT_NEEDED |
Support native ELF only |
| Resolver | Map names to paths | Emulate ld.so order |
| Graph builder | Store nodes/edges | Stable sort for determinism |
| Output writer | DOT/SVG/ASCII | DOT is source of truth |
4.3 Data Structures (No Full Code)
typedef struct {
char* soname;
char* path;
int missing;
} node_t;
typedef struct {
int from;
int to;
int is_cycle;
} edge_t;
4.4 Algorithm Overview
Key Algorithm: Dependency Resolution
- Parse
DT_NEEDEDfor a node. - For each name, resolve path with search order.
- Add edge; if node not visited, recurse.
- Sort nodes/edges before output.
Complexity Analysis:
- Time: O(V + E) + sorting cost.
- Space: O(V + E).
5. Implementation Guide
5.1 Development Environment Setup
sudo apt-get install build-essential graphviz
5.2 Project Structure
libviz/
|-- src/
| |-- main.c
| |-- elf_parser.c
| |-- resolver.c
| |-- graph.c
| `-- output.c
|-- tests/
| |-- fixtures/
| `-- golden/
|-- Makefile
`-- README.md
5.3 The Core Question You’re Answering
“Can I replicate the loader’s dependency resolution and explain it visually?”
5.4 Concepts You Must Understand First
- Dynamic section parsing
- Loader search order
- Graph traversal and cycles
5.5 Questions to Guide Your Design
- How will you handle unresolved dependencies in the graph?
- What is your canonical node identifier (path or SONAME)?
- How will you keep output deterministic?
5.6 Thinking Exercise
Design a dependency graph for a binary where libA and libB both depend on libC. How will you avoid duplicating libC?
5.7 The Interview Questions They’ll Ask
- “Where does
DT_NEEDEDlive in ELF?” - “What is the difference between RUNPATH and RPATH?”
- “How do you avoid cycles in dependency graphs?”
5.8 Hints in Layers
Hint 1: Start with readelf output
readelf -d ./app | grep NEEDED
Hint 2: Use a visited set
Hint 3: Emit DOT first, then convert to SVG
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| ELF metadata | TLPI | Ch. 41 |
| Dynamic linking | Linkers and Loaders | Ch. 10 |
| Graph traversal | Algorithms (Sedgewick) | Graphs |
5.10 Implementation Phases
Phase 1: Foundation (2-3 days)
- Parse ELF and list
DT_NEEDED. - Print dependencies for one binary.
Phase 2: Core Functionality (4-5 days)
- Implement search path resolver.
- Build graph and detect cycles.
Phase 3: Polish & Edge Cases (2-3 days)
- Add DOT/SVG output and deterministic ordering.
- Add missing dependency placeholders.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Node ID | Path vs SONAME | Path + SONAME metadata | Avoid duplicates |
| Output | DOT only vs multiple | DOT as source, convert | Simpler tests |
| Env usage | Use env vs ignore | Flag-controlled | Deterministic tests |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | ELF parsing | DT_NEEDED extraction |
| Integration Tests | Resolver + graph | Compare to golden DOT |
| Edge Case Tests | Missing deps, cycles | Placeholder nodes |
6.2 Critical Test Cases
- Binary with
RUNPATHoverriding defaults. - Missing dependency -> graph includes missing node.
- Cycle detection -> no infinite recursion.
6.3 Test Data
fixtures/app
fixtures/libs/libfoo.so.1
fixtures/libs/libbar.so.1
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Parsing wrong class | Garbage names | Validate ELF class and endianness |
| Ignoring RUNPATH | Graph mismatch | Implement correct precedence |
| Non-deterministic order | Unstable diffs | Sort nodes/edges |
7.2 Debugging Strategies
- Use
readelf -das a ground truth. - Compare resolved paths with
LD_DEBUG=libsoutput.
7.3 Performance Traps
- Re-parsing the same library multiple times without caching.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add
--jsonoutput format. - Add
--max-depthto limit recursion.
8.2 Intermediate Extensions
- Include symbol-level dependencies (from
.dynsym). - Highlight missing libs in red in SVG.
8.3 Advanced Extensions
- Support Mach-O and PE formats.
- Add SBOM output for dependencies.
9. Real-World Connections
9.1 Industry Applications
- Packaging and deployment validation.
- Security audits of runtime dependency trees.
9.2 Related Open Source Projects
- ldd: loader-based dependency display.
- pax-utils (lddtree): richer dependency analysis.
9.3 Interview Relevance
- Demonstrates understanding of loaders and ELF internals.
10. Resources
10.1 Essential Reading
- “The Linux Programming Interface” (Kerrisk), Ch. 41.
- “Linkers and Loaders” (Levine), Ch. 10.
10.2 Video Resources
- “ELF Internals” conference talks.
10.3 Tools & Documentation
readelf,objdump,ldd,ldconfig.
10.4 Related Projects in This Series
- P01-plugin-audio-effects-processor: uses loader APIs.
- P06-minimal-dynamic-linker: builds on ELF parsing.
11. Self-Assessment Checklist
11.1 Understanding
- I can explain
DT_NEEDEDandDT_RUNPATH. - I can describe loader search order accurately.
- I can explain why dependency graphs can contain cycles.
11.2 Implementation
- Output is deterministic across runs.
- Missing dependencies are clearly marked.
- DOT output renders correctly to SVG.
11.3 Growth
- I can explain loader resolution to a teammate.
- I documented at least one tricky dependency issue.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Parse ELF and list
DT_NEEDEDdependencies. - Resolve paths using at least system directories.
- Output DOT graph.
Full Completion:
- Full search order emulation with
RPATH/RUNPATHandLD_LIBRARY_PATHoverrides. - Deterministic output with golden tests.
Excellence (Going Above & Beyond):
- Cross-platform format support.
- Visual diff tool for comparing graphs across machines.