diff --git a/libc-test/build.rs b/libc-test/build.rs
index 4025027a35e9b9c2fe522ef825b4a0cf595d53af..45e0b0836804cbdf1a3933ac6ddb3142d8282f7a 100644
--- a/libc-test/build.rs
+++ b/libc-test/build.rs
@@ -21,9 +21,7 @@ fn do_ctest() {
     let emscripten = target.contains("asm");
     let musl = target.contains("musl") || emscripten;
     let uclibc = target.contains("uclibc");
-    let freebsd = target.contains("freebsd");
     let mips = target.contains("mips");
-    let bsdlike = freebsd;
     let mut cfg = ctest::TestGenerator::new();
 
     match &target {
@@ -37,15 +35,12 @@ fn do_ctest() {
         t if t.contains("dragonfly") => return test_dragonflybsd(t),
         t if t.contains("wasi") => return test_wasi(t),
         t if t.contains("android") => return test_android(t),
+        t if t.contains("freebsd") => return test_freebsd(t),
         _ => (),
     }
 
     // Pull in extra goodies
-    if linux || emscripten {
-        cfg.define("_GNU_SOURCE", None);
-    } else if freebsd {
-        cfg.define("_WITH_GETLINE", None);
-    }
+    cfg.define("_GNU_SOURCE", None);
 
     cfg.header("errno.h")
         .header("fcntl.h")
@@ -112,9 +107,7 @@ fn do_ctest() {
     cfg.header("ifaddrs.h");
     cfg.header("langinfo.h");
 
-    if !freebsd {
-        cfg.header("sys/quota.h");
-    }
+    cfg.header("sys/quota.h");
 
     if !musl && !x32 {
         cfg.header("sys/sysctl.h");
@@ -128,17 +121,6 @@ fn do_ctest() {
         cfg.header("utmpx.h");
     }
 
-    if bsdlike {
-        cfg.header("sys/event.h");
-        cfg.header("net/if_dl.h");
-        if freebsd {
-            cfg.header("net/bpf.h");
-            cfg.header("libutil.h");
-        } else {
-            cfg.header("util.h");
-        }
-    }
-
     if linux || emscripten {
         cfg.header("mntent.h");
         cfg.header("mqueue.h");
@@ -232,22 +214,7 @@ fn do_ctest() {
         cfg.header("spawn.h");
     }
 
-    if freebsd {
-        cfg.header("mqueue.h");
-        cfg.header("pthread_np.h");
-        cfg.header("sched.h");
-        cfg.header("ufs/ufs/quota.h");
-        cfg.header("sys/extattr.h");
-        cfg.header("sys/jail.h");
-        cfg.header("sys/ipc.h");
-        cfg.header("sys/msg.h");
-        cfg.header("sys/shm.h");
-        cfg.header("sys/procdesc.h");
-        cfg.header("sys/rtprio.h");
-        cfg.header("spawn.h");
-    }
-
-    if linux || freebsd || emscripten {
+    if linux || emscripten {
         if !uclibc {
             cfg.header("aio.h");
         }
@@ -261,9 +228,6 @@ fn do_ctest() {
             | "Elf64_Sym" | "Elf32_Ehdr" | "Elf64_Ehdr" | "Elf32_Chdr"
             | "Elf64_Chdr" => ty.to_string(),
 
-            // OSX calls this something else
-            "sighandler_t" if bsdlike => "sig_t".to_string(),
-
             t if is_union => format!("union {}", t),
 
             t if t.ends_with("_t") => t.to_string(),
@@ -284,7 +248,7 @@ fn do_ctest() {
             }
             "u64" if struct_ == "epoll_event" => "data.u64".to_string(),
             "type_"
-                if (linux || freebsd)
+                if linux
                     && (struct_ == "input_event"
                         || struct_ == "input_mask"
                         || struct_ == "ff_effect"
@@ -329,17 +293,10 @@ fn do_ctest() {
         }
     });
 
-    cfg.skip_signededness(move |c| {
-        match c {
-            "LARGE_INTEGER" | "float" | "double" => true,
-            n if n.starts_with("pthread") => true,
-            // sem_t is a struct or pointer
-            "sem_t" if freebsd => true,
-            // mqd_t is a pointer on FreeBSD
-            "mqd_t" if freebsd => true,
-
-            _ => false,
-        }
+    cfg.skip_signededness(move |c| match c {
+        "LARGE_INTEGER" | "float" | "double" => true,
+        n if n.starts_with("pthread") => true,
+        _ => false,
     });
 
     cfg.skip_const(move |name| {
@@ -367,31 +324,6 @@ fn do_ctest() {
             "MS_NOUSER" => true,
             "MS_RMT_MASK" => true, // updated in glibc 2.22 and musl 1.1.13
 
-            // These constants were removed in FreeBSD 11 (svn r273250) but will
-            // still be accepted and ignored at runtime.
-            "MAP_RENAME" | "MAP_NORESERVE" if freebsd => true,
-
-            // These constants were removed in FreeBSD 11 (svn r262489),
-            // and they've never had any legitimate use outside of the
-            // base system anyway.
-            "CTL_MAXID" | "KERN_MAXID" | "HW_MAXID" | "NET_MAXID"
-            | "USER_MAXID"
-                if freebsd =>
-            {
-                true
-            }
-
-            // These constants were added in FreeBSD 11
-            "EVFILT_PROCDESC" | "EVFILT_SENDFILE" | "EVFILT_EMPTY"
-            | "PD_CLOEXEC" | "PD_ALLOWED_AT_FORK"
-                if freebsd =>
-            {
-                true
-            }
-
-            // These constants were added in FreeBSD 12
-            "SF_USER_READAHEAD" | "SO_REUSEPORT_LB" if freebsd => true,
-
             // These are either unimplemented or optionally built into uClibc
             "LC_CTYPE_MASK"
             | "LC_NUMERIC_MASK"
@@ -499,16 +431,6 @@ fn do_ctest() {
                 true
             }
 
-            | "IP_ORIGDSTADDR"
-            | "IP_RECVORIGDSTADDR"
-            | "IPV6_ORIGDSTADDR"
-            | "IPV6_RECVORIGDSTADDR"
-                if freebsd =>
-            {
-                // FreeBSD 12 required, but CI has FreeBSD 11.
-                true
-            }
-
             _ => false,
         }
     });
@@ -533,7 +455,7 @@ fn do_ctest() {
             "sendmmsg" | "recvmmsg" if musl => true,
 
             // typed 2nd arg on linux
-            "gettimeofday" if linux || freebsd => true,
+            "gettimeofday" if linux => true,
 
             "dladdr" if musl => true, // const-ness only added recently
 
@@ -553,20 +475,6 @@ fn do_ctest() {
             // [3]: https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/sys/eventfd.h;h=6295f32e937e779e74318eb9d3bdbe76aef8a8f3;hb=4e42b5b8f89f0e288e68be7ad70f9525aebc2cff#l34
             "eventfd" if linux => true,
 
-            // The `uname` function in freebsd is now an inline wrapper that
-            // delegates to another, but the symbol still exists, so don't check
-            // the symbol.
-            "uname" if freebsd => true,
-
-            // FIXME: need to upgrade FreeBSD version; see https://github.com/rust-lang/libc/issues/938
-            "setgrent" if freebsd => true,
-
-            // aio_waitcomplete's return type changed between FreeBSD 10 and 11.
-            "aio_waitcomplete" if freebsd => true,
-
-            // lio_listio confuses the checker, probably because one of its
-            // arguments is an array
-            "lio_listio" if freebsd => true,
             "lio_listio" if musl => true,
 
             // These are either unimplemented or optionally built into uClibc
@@ -600,8 +508,6 @@ fn do_ctest() {
         (struct_ == "sigevent" && field == "sigev_value") ||
         // aio_buf is "volatile void*" and Rust doesn't understand volatile
         (struct_ == "aiocb" && field == "aio_buf") ||
-        // stack_t.ss_sp's type changed from FreeBSD 10 to 11 in svn r294930
-        (freebsd && struct_ == "stack_t" && field == "ss_sp") ||
         // this one is an anonymous union
         (linux && struct_ == "ff_effect" && field == "u")
     });
@@ -2209,3 +2115,320 @@ fn test_android(target: &str) {
     });
     cfg.generate("../src/lib.rs", "linux_fcntl.rs");
 }
+
+fn test_freebsd(target: &str) {
+    assert!(target.contains("freebsd"));
+    let x86 = target.contains("i686") || target.contains("x86_64");
+
+    let mut cfg = ctest::TestGenerator::new();
+    // FIXME: still necessary?
+    cfg.define("_WITH_GETLINE", None);
+
+    // FIXME: still necessary?
+    cfg.flag("-Wno-deprecated-declarations");
+
+    headers! { cfg:
+                "aio.h",
+                "arpa/inet.h",
+                "ctype.h",
+                "dirent.h",
+                "dlfcn.h",
+                "errno.h",
+                "fcntl.h",
+                "glob.h",
+                "grp.h",
+                "ifaddrs.h",
+                "langinfo.h",
+                "libutil.h",
+                "limits.h",
+                "locale.h",
+                "mqueue.h",
+                "net/bpf.h",
+                "net/if.h",
+                "net/if_arp.h",
+                "net/if_dl.h",
+                "net/route.h",
+                "netdb.h",
+                "netinet/in.h",
+                "netinet/tcp.h",
+                "netinet/udp.h",
+                "poll.h",
+                "pthread.h",
+                "pthread_np.h",
+                "pwd.h",
+                "resolv.h",
+                "sched.h",
+                "semaphore.h",
+                "signal.h",
+                "spawn.h",
+                "stddef.h",
+                "stdint.h",
+                "stdio.h",
+                "stdlib.h",
+                "string.h",
+                "sys/event.h",
+                "sys/extattr.h",
+                "sys/file.h",
+                "sys/ioctl.h",
+                "sys/ipc.h",
+                "sys/jail.h",
+                "sys/mman.h",
+                "sys/mount.h",
+                "sys/msg.h",
+                "sys/procdesc.h",
+                "sys/ptrace.h",
+                "sys/resource.h",
+                "sys/rtprio.h",
+                "sys/shm.h",
+                "sys/socket.h",
+                "sys/stat.h",
+                "sys/statvfs.h",
+                "sys/time.h",
+                "sys/times.h",
+                "sys/types.h",
+                "sys/uio.h",
+                "sys/un.h",
+                "sys/utsname.h",
+                "sys/wait.h",
+                "syslog.h",
+                "termios.h",
+                "time.h",
+                "ufs/ufs/quota.h",
+                "unistd.h",
+                "utime.h",
+                "wchar.h",
+    }
+
+    cfg.type_name(move |ty, is_struct, is_union| {
+        match ty {
+            // Just pass all these through, no need for a "struct" prefix
+            // FIXME: still required?
+            "FILE" | "fd_set" | "Dl_info" | "DIR" | "Elf32_Phdr"
+            | "Elf64_Phdr" | "Elf32_Shdr" | "Elf64_Shdr" | "Elf32_Sym"
+            | "Elf64_Sym" | "Elf32_Ehdr" | "Elf64_Ehdr" | "Elf32_Chdr"
+            | "Elf64_Chdr" => ty.to_string(),
+
+            // FIXME: still required?
+            "sighandler_t" => "sig_t".to_string(),
+
+            t if is_union => format!("union {}", t),
+
+            t if t.ends_with("_t") => t.to_string(),
+
+            // put `struct` in front of all structs:.
+            t if is_struct => format!("struct {}", t),
+
+            t => t.to_string(),
+        }
+    });
+
+    cfg.field_name(move |struct_, field| {
+        match field {
+            // Our stat *_nsec fields normally don't actually exist but are part
+            // of a timeval struct
+            s if s.ends_with("_nsec") && struct_.starts_with("stat") => {
+                s.replace("e_nsec", ".tv_nsec")
+            }
+            // FIXME: still required?
+            "u64" if struct_ == "epoll_event" => "data.u64".to_string(),
+            // FIXME: still required?
+            "type_"
+                if struct_ == "input_event"
+                    || struct_ == "input_mask"
+                    || struct_ == "ff_effect"
+                    || struct_ == "rtprio" =>
+            {
+                "type".to_string()
+            }
+            s => s.to_string(),
+        }
+    });
+
+    cfg.skip_type(move |ty| {
+        match ty {
+            // sighandler_t is crazy across platforms
+            // FIXME: still required?
+            "sighandler_t" => true,
+
+            _ => false,
+        }
+    });
+
+    cfg.skip_struct(move |ty| {
+        match ty {
+            // This is actually a union, not a struct
+            // FIXME: still required?
+            "sigval" => true,
+
+            // These are tested as part of the linux_fcntl tests since there are
+            // header conflicts when including them with all the other structs.
+            // FIXME: still required?
+            "termios2" => true,
+
+            _ => false,
+        }
+    });
+
+    cfg.skip_signededness(move |c| {
+        match c {
+            // FIXME: still required?
+            "LARGE_INTEGER" | "float" | "double" => true,
+            // FIXME: still required?
+            n if n.starts_with("pthread") => true,
+            // sem_t is a struct or pointer
+            // FIXME: still required?
+            "sem_t" => true,
+            // mqd_t is a pointer on FreeBSD
+            // FIXME: still required?
+            "mqd_t" => true,
+
+            _ => false,
+        }
+    });
+
+    cfg.skip_const(move |name| {
+        match name {
+            // FIXME: still required?
+            "SIG_DFL" | "SIG_ERR" | "SIG_IGN" => true, // sighandler_t weirdness
+            // FIXME: still required?
+            "SIGUNUSED" => true, // removed in glibc 2.26
+
+            // weird signed extension or something like that?
+            // FIXME: still required?
+            "MS_NOUSER" => true,
+            // FIXME: still required?
+            "MS_RMT_MASK" => true, // updated in glibc 2.22 and musl 1.1.13
+
+            // These constants were removed in FreeBSD 11 (svn r273250) but will
+            // still be accepted and ignored at runtime.
+            "MAP_RENAME" | "MAP_NORESERVE" => true,
+
+            // These constants were removed in FreeBSD 11 (svn r262489),
+            // and they've never had any legitimate use outside of the
+            // base system anyway.
+            "CTL_MAXID" | "KERN_MAXID" | "HW_MAXID" | "NET_MAXID"
+            | "USER_MAXID" => true,
+
+            // These constants were added in FreeBSD 11
+            // FIXME: still required?
+            "EVFILT_PROCDESC" | "EVFILT_SENDFILE" | "EVFILT_EMPTY"
+            | "PD_CLOEXEC" | "PD_ALLOWED_AT_FORK" => true,
+
+            // These constants were added in FreeBSD 12
+            // FIXME: still required?
+            "SF_USER_READAHEAD" | "SO_REUSEPORT_LB" => true,
+
+            // These constants are tested in a separate test program generated
+            // below because there are header conflicts if we try to include the
+            // headers that define them here.
+            // FIXME: still required?
+            "F_CANCELLK" | "F_ADD_SEALS" | "F_GET_SEALS" => true,
+            // FIXME: still required?
+            "F_SEAL_SEAL" | "F_SEAL_SHRINK" | "F_SEAL_GROW"
+            | "F_SEAL_WRITE" => true,
+            // FIXME: still required?
+            "BOTHER" => true,
+
+            // MFD_HUGETLB is not available in some older libc versions on the
+            // CI builders. On the x86_64 and i686 builders it seems to be
+            // available for all targets, so at least test it there.
+            // FIXME: still required?
+            "MFD_HUGETLB" if !x86 => true,
+
+            // These change all the time from release to release of linux
+            // distros, let's just not bother trying to verify them. They
+            // shouldn't be used in code anyway...
+            // FIXME: still required?
+            "AF_MAX" | "PF_MAX" => true,
+
+            // FreeBSD 12 required, but CI has FreeBSD 11.
+            // FIXME: still required?
+            "IP_ORIGDSTADDR"
+            | "IP_RECVORIGDSTADDR"
+            | "IPV6_ORIGDSTADDR"
+            | "IPV6_RECVORIGDSTADDR" => true,
+
+            _ => false,
+        }
+    });
+
+    cfg.skip_fn(move |name| {
+        // skip those that are manually verified
+        match name {
+            // FIXME: still required?
+            "execv" |       // crazy stuff with const/mut
+            "execve" |
+            "execvp" |
+            "execvpe" |
+            "fexecve" => true,
+
+            // The `uname` function in freebsd is now an inline wrapper that
+            // delegates to another, but the symbol still exists, so don't check
+            // the symbol.
+            // FIXME: still required?
+            "uname" => true,
+
+            // FIXME: need to upgrade FreeBSD version; see https://github.com/rust-lang/libc/issues/938
+            // FIXME: still required?
+            "setgrent" => true,
+
+            // aio_waitcomplete's return type changed between FreeBSD 10 and 11.
+            // FIXME: still required?
+            "aio_waitcomplete"  => true,
+
+            // lio_listio confuses the checker, probably because one of its
+            // arguments is an array
+            // FIXME: still required?
+            "lio_listio" => true,
+
+            // Definition of those functions as changed since unified headers from NDK r14b
+            // These changes imply some API breaking changes but are still ABI compatible.
+            // We can wait for the next major release to be compliant with the new API.
+            // FIXME: unskip these for next major release
+            // FIXME: still required ?
+            "strerror_r" | "madvise" | "msync" | "mprotect" | "recvfrom" | "getpriority" |
+
+            _ => false,
+        }
+    });
+
+    cfg.skip_field_type(move |struct_, field| {
+        // This is a weird union, don't check the type.
+        // FIXME: still required?
+        (struct_ == "ifaddrs" && field == "ifa_ifu") ||
+        // FIXME: still required?
+        // sighandler_t type is super weird
+            (struct_ == "sigaction" && field == "sa_sigaction") ||
+        // FIXME: still required?
+        // sigval is actually a union, but we pretend it's a struct
+            (struct_ == "sigevent" && field == "sigev_value") ||
+        // aio_buf is "volatile void*" and Rust doesn't understand volatile
+        // FIXME: still required?
+            (struct_ == "aiocb" && field == "aio_buf") ||
+        // stack_t.ss_sp's type changed from FreeBSD 10 to 11 in svn r294930
+        // FIXME: still required?
+            (struct_ == "stack_t" && field == "ss_sp")
+    });
+
+    cfg.skip_field(move |struct_, field| {
+        // this is actually a union on linux, so we can't represent it well and
+        // just insert some padding.
+        // FIXME: still required?
+        (struct_ == "siginfo_t" && field == "_pad") ||
+        // sigev_notify_thread_id is actually part of a sigev_un union
+        // FIXME: still required?
+        (struct_ == "sigevent" && field == "sigev_notify_thread_id") ||
+        // signalfd had SIGSYS fields added in Linux 4.18, but no libc release has them yet.
+        // FIXME: still required?
+        (struct_ == "signalfd_siginfo" && (field == "ssi_addr_lsb" ||
+                                           field == "_pad2" ||
+                                           field == "ssi_syscall" ||
+                                           field == "ssi_call_addr" ||
+                                           field == "ssi_arch"))
+    });
+
+    // FIXME: remove
+    cfg.fn_cname(move |name, _cname| name.to_string());
+
+    cfg.generate("../src/lib.rs", "main.rs");
+}