diff --git a/debian/changelog b/debian/changelog
index 5373b1906dfedaade4e740bdab0aab068fb433e3..f991897ef9590e9599a4fe3c41fc1751168c3bf3 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+dwarf2sources (0.2.4) apertis; urgency=medium
+
+  * Collect project-external files from debug line info
+
+ -- Ryan Gonzalez <ryan.gonzalez@collabora.com>  Thu, 21 Sep 2023 15:50:00 -0500
+
 dwarf2sources (0.2.3) apertis; urgency=medium
 
   * Add support for reading DWARF-5 info
diff --git a/src/main.rs b/src/main.rs
index 52cbd098adea484ac7cdea3a776cf19830538a04..bc2bc979adbe33580c6103fce9ef488fe2df6aa6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,17 +1,32 @@
 // Based on the gimli 0.16.1 dwarfdump.rs example
-use anyhow::{anyhow, Error, Result};
+use anyhow::{anyhow, bail, Error, Result};
 use fallible_iterator::{convert, FallibleIterator};
 use gimli::{AttributeValue, Endianity, Reader};
 use object::{Object, ObjectSection};
 use serde::ser::SerializeMap;
 use serde::{Serialize, Serializer};
 use std::borrow::{Borrow, Cow};
+use std::collections::BTreeSet;
 use std::fs;
 use std::path::PathBuf;
 use structopt::StructOpt;
 use typed_arena::Arena;
 
-fn list_file<E: Endianity>(file: &object::File, endian: E) -> Result<Vec<Unit>> {
+#[derive(Debug, Serialize)]
+struct Unit {
+    comp_dir: String,
+    comp_name: String,
+}
+
+#[derive(Debug, Default, Serialize)]
+struct ObjectInfo {
+    units: Vec<Unit>,
+    // Note that we use a BTreeSet so that the output order is consistent across
+    // runs.
+    external_files: BTreeSet<String>,
+}
+
+fn list_file<E: Endianity>(file: &object::File, endian: E) -> Result<ObjectInfo> {
     let arena = Arena::new();
 
     fn load_section<'a, 'file, 'input, S, E>(
@@ -39,33 +54,61 @@ fn list_file<E: Endianity>(file: &object::File, endian: E) -> Result<Vec<Unit>>
     let debug_info = &load_section(&arena, file, endian);
     let debug_str = &load_section(&arena, file, endian);
     let debug_line_str = &load_section(&arena, file, endian);
+    let debug_line = &load_section(&arena, file, endian);
 
-    list_info(debug_info, debug_abbrev, debug_str, debug_line_str)
+    list_info(
+        debug_info,
+        debug_abbrev,
+        debug_str,
+        debug_line_str,
+        debug_line,
+    )
 }
 
 fn list_info<R: Reader>(
     debug_info: &gimli::DebugInfo<R>,
     debug_abbrev: &gimli::DebugAbbrev<R>,
     debug_str: &gimli::DebugStr<R>,
+    debug_line: &gimli::DebugLine<R>,
     debug_line_str: &gimli::DebugLineStr<R>,
-) -> Result<Vec<Unit>> {
-    let mut v = Vec::new();
-    let units = debug_info.units().collect::<Vec<_>>().unwrap();
-    for u in units {
+) -> Result<ObjectInfo> {
+    let mut info = ObjectInfo::default();
+    let dw_units = debug_info.units().collect::<Vec<_>>().unwrap();
+    for u in dw_units {
         let abbrevs = u.abbreviations(debug_abbrev)?;
-        v.append(&mut list_entries(
+        info.units.append(&mut list_entries(
+            &mut info.external_files,
             u.entries(&abbrevs),
+            u.address_size(),
             debug_str,
             debug_line_str,
+            debug_line,
         )?);
     }
-    Ok(v)
+    Ok(info)
 }
 
-#[derive(Debug, Serialize)]
-struct Unit {
-    comp_dir: String,
-    comp_name: String,
+fn get_attr_string<R: Reader>(
+    dw_attr_value: &gimli::AttributeValue<R>,
+    debug_str: &gimli::DebugStr<R>,
+    debug_line_str: &gimli::DebugLineStr<R>,
+) -> Result<(R, String)> {
+    let reader = if let Some(r) = dw_attr_value.string_value(debug_str) {
+        r
+    } else {
+        match dw_attr_value {
+            AttributeValue::DebugLineStrRef(offset) => debug_line_str
+                .get_str(*offset)
+                .expect("attribute value should be set"),
+            AttributeValue::DebugStrRefSup(_) => {
+                bail!("attribute in supplemental file");
+            }
+            _ => bail!("attribute with unexpected type"),
+        }
+    };
+
+    let string = reader.to_string()?.into_owned();
+    Ok((reader, string))
 }
 
 fn entry_attr_value<R: Reader>(
@@ -73,36 +116,72 @@ fn entry_attr_value<R: Reader>(
     attr_name: gimli::constants::DwAt,
     debug_str: &gimli::DebugStr<R>,
     debug_line_str: &gimli::DebugLineStr<R>,
-) -> Result<Option<String>> {
+) -> Result<Option<(R, String)>> {
     let dw_attr = if let Some(it) = entry.attr(attr_name)? {
         it
     } else {
         return Ok(None);
     };
 
-    if let Some(r) = dw_attr.string_value(debug_str) {
-        Ok(Some(r.to_string()?.into_owned()))
+    get_attr_string(&dw_attr.value(), debug_str, debug_line_str).map(Some)
+}
+
+fn collect_external_files<R: Reader>(
+    external_files: &mut BTreeSet<String>,
+    entry: &gimli::DebuggingInformationEntry<R>,
+    address_size: u8,
+    debug_str: &gimli::DebugStr<R>,
+    debug_line_str: &gimli::DebugLineStr<R>,
+    debug_line: &gimli::DebugLine<R>,
+    comp_dir_reader: R,
+    comp_name_reader: R,
+) -> Result<()> {
+    let program = if let Some(gimli::AttributeValue::DebugLineRef(offs)) =
+        entry.attr_value(gimli::DW_AT_stmt_list)?
+    {
+        debug_line.program(
+            offs,
+            address_size,
+            Some(comp_dir_reader),
+            Some(comp_name_reader),
+        )?
     } else {
-        match dw_attr.raw_value() {
-            AttributeValue::DebugLineStrRef(offset) => Ok(Some(
-                debug_line_str
-                    .get_str(offset)
-                    .expect("attribute value should be set")
-                    .to_string()?
-                    .into_owned(),
-            )),
-            AttributeValue::DebugStrRefSup(_) => {
-                Err(anyhow!("attribute {} in supplemental file", attr_name))
-            }
-            _ => Err(anyhow!("attribute {} with unexpected type", attr_name)),
+        return Ok(());
+    };
+
+    let header = program.header();
+    for file_entry in header.file_names() {
+        if file_entry.directory_index() == 0 {
+            // Directory index of 0 means it's from the current CU's directory,
+            // so skip it.
+            continue;
+        }
+
+        let mut name =
+            PathBuf::from(get_attr_string(&file_entry.path_name(), debug_str, debug_line_str)?.1);
+
+        if let Some(dir_attr) = file_entry.directory(header) {
+            let (_, dir) = get_attr_string(&dir_attr, debug_str, debug_line_str)?;
+            name = PathBuf::from(dir).join(name);
+        }
+
+        if name.is_absolute() {
+            // This shouldn't be invalid UTF-8, because it just came from some
+            // String instances already.
+            external_files.insert(name.into_os_string().into_string().unwrap());
         }
     }
+
+    Ok(())
 }
 
 fn list_entries<R: Reader>(
+    external_files: &mut BTreeSet<String>,
     mut entries: gimli::EntriesCursor<R>,
+    address_size: u8,
     debug_str: &gimli::DebugStr<R>,
     debug_line_str: &gimli::DebugLineStr<R>,
+    debug_line: &gimli::DebugLine<R>,
 ) -> Result<Vec<Unit>> {
     let mut depth = 0;
     let mut v = Vec::new();
@@ -114,13 +193,13 @@ fn list_entries<R: Reader>(
         }
 
         if entry.tag() == gimli::DW_TAG_compile_unit || entry.tag() == gimli::DW_TAG_type_unit {
-            let comp_dir =
+            let (comp_dir_reader, comp_dir) =
                 entry_attr_value(entry, gimli::DW_AT_comp_dir, debug_str, debug_line_str)?
                     .ok_or_else(|| anyhow!("Unable to parse or missing DW_AT_comp_dir"))?;
 
             let at_name = entry_attr_value(entry, gimli::DW_AT_name, debug_str, debug_line_str);
-            let comp_name = match at_name {
-                Ok(Some(comp_name)) => comp_name.to_string(),
+            let (comp_name_reader, comp_name) = match at_name {
+                Ok(Some(result)) => result,
                 Ok(None) => {
                     eprintln!("Warning: unit without name, skipping it");
                     continue;
@@ -139,16 +218,24 @@ fn list_entries<R: Reader>(
                     comp_name,
                 });
             }
+
+            if let Err(err) = collect_external_files(
+                external_files,
+                entry,
+                address_size,
+                debug_str,
+                debug_line_str,
+                debug_line,
+                comp_dir_reader,
+                comp_name_reader,
+            ) {
+                eprintln!("Warning: collecting external files: {}", err);
+            }
         }
     }
     Ok(v)
 }
 
-#[derive(Debug, Serialize)]
-struct Data {
-    units: Vec<Unit>,
-}
-
 #[derive(StructOpt, Debug)]
 struct Opt {
     #[structopt(short, long, parse(from_os_str))]
@@ -164,28 +251,17 @@ fn process_file<E: Endianity>(
     file: &object::File,
     endian: E,
     strip_prefix: Option<&String>,
-) -> Result<Vec<Unit>> {
-    let units = list_file(file, endian)?;
-    let units = if let Some(strip) = strip_prefix {
-        units
-            .iter()
-            .map(|u| {
-                let comp_dir = if u.comp_dir.starts_with(strip) {
-                    (&u.comp_dir[strip.len()..]).to_string()
-                } else {
-                    u.comp_dir.clone()
-                };
-                Unit {
-                    comp_dir,
-                    comp_name: u.comp_name.clone(),
-                }
-            })
-            .collect()
-    } else {
-        units
-    };
+) -> Result<ObjectInfo> {
+    let mut info = list_file(file, endian)?;
+    if let Some(strip) = strip_prefix {
+        for u in info.units.iter_mut() {
+            if u.comp_dir.starts_with(strip) {
+                u.comp_dir = (&u.comp_dir[strip.len()..]).to_string();
+            }
+        }
+    }
 
-    Ok(units)
+    Ok(info)
 }
 
 fn serialize<S, K, V, I>(s: S, mut iter: I, len: Option<usize>) -> Result<()>
@@ -218,8 +294,8 @@ fn main() -> Result<()> {
             gimli::RunTimeEndian::Big
         };
 
-        let units = process_file(&file, endian, opt.strip_prefix.as_ref())?;
-        Ok((f.clone(), Data { units }))
+        let info = process_file(&file, endian, opt.strip_prefix.as_ref())?;
+        Ok((f.clone(), info))
     }));
 
     if let Some(output) = &opt.output {