2 Commits

Author SHA1 Message Date
Spencer Brower
f6cfb4a98d chore(dev): release 0.4.0 2026-06-16 23:33:11 -04:00
1fc5f8280e feat: Replaced fd with custom internals. 2026-06-16 23:32:48 -04:00
2 changed files with 226 additions and 82 deletions

View File

@@ -5,7 +5,7 @@
### Features
* Replaced `fd` with custom internals. ([ba647f5](https://github.com/sbrow/envr/commit/ba647f51c17b694033b4f13c06b38071305cc09d))
* Replaced `fd` with custom internals. ([1fc5f82](https://github.com/sbrow/envr/commit/1fc5f8280e4a67b67bc14c08eb50ec4334e09cda))
## [0.3.0](https://github.com/sbrow/envr/compare/v0.2.1...v0.3.0) (2026-06-16)

View File

@@ -3,7 +3,12 @@ package findr
import "core:fmt"
import "core:os"
import "core:strings"
import "core:sync"
import "core:sys/linux"
import "core:thread"
FINDR_PARALLEL :: #config(FINDR_PARALLEL, false)
FINDR_THREADS :: #config(FINDR_THREADS, 8)
RawEntry :: struct {
name: string,
@@ -11,106 +16,245 @@ RawEntry :: struct {
}
walk :: proc(root: string, results: ^[dynamic]string) {
walk_dir(root, results)
when FINDR_PARALLEL {
walk_parallel(root, results)
} else {
walk_dir_serial(root, results)
}
}
walk_dir :: proc(dir_path: string, results: ^[dynamic]string) {
cpath := strings.clone_to_cstring(dir_path)
if cpath == nil do return
defer delete(cpath)
read_dir_entries :: proc(dir_path: string, has_git: ^bool) -> [dynamic]RawEntry {
entries := make([dynamic]RawEntry)
fd, err := linux.open(cpath, {.DIRECTORY, .CLOEXEC})
if err != .NONE do return
defer linux.close(fd)
cpath := strings.clone_to_cstring(dir_path)
if cpath == nil do return entries
buf: [8192]u8
has_git := false
fd, err := linux.open(cpath, {.DIRECTORY, .CLOEXEC})
delete(cpath)
if err != .NONE do return entries
entries := make([dynamic]RawEntry)
defer {
for &entry in entries {
delete(entry.name)
}
delete(entries)
}
buf: [8192]u8
has_git^ = false
for {
n, errno := linux.getdents(fd, buf[:])
if n <= 0 || errno != .NONE do break
for {
n, errno := linux.getdents(fd, buf[:])
if n <= 0 || errno != .NONE do break
offs := 0
for d in linux.dirent_iterate_buf(buf[:n], &offs) {
name := linux.dirent_name(d)
if name == "." || name == ".." do continue
offs := 0
for d in linux.dirent_iterate_buf(buf[:n], &offs) {
name := linux.dirent_name(d)
if name == "." || name == ".." do continue
if name == ".git" && d.type == .DIR {
has_git = true
}
if name == ".git" && d.type == .DIR {
has_git^ = true
}
cloned := strings.clone(name)
append(&entries, RawEntry{name = cloned, type = d.type})
}
}
cloned := strings.clone(name)
append(&entries, RawEntry{name = cloned, type = d.type})
}
}
if has_git {
gi := load_gitignore(dir_path)
defer if gi != nil {
destroy(gi)
free(gi)
}
linux.close(fd)
return entries
}
for entry in entries {
if entry.name == ".git" do continue
is_dir := entry.type == .DIR
if gi != nil && is_ignored(gi, entry.name, is_dir) {
if !is_dir {
full_path := join_path(dir_path, entry.name)
append(results, full_path)
}
continue
}
if is_dir {
child_path := join_path(dir_path, entry.name)
walk_dir(child_path, results)
delete(child_path)
}
}
} else {
for entry in entries {
if entry.type == .DIR {
child_path := join_path(dir_path, entry.name)
walk_dir(child_path, results)
delete(child_path)
}
}
}
free_entries :: proc(entries: ^[dynamic]RawEntry) {
for &entry in entries {
delete(entry.name)
}
delete(entries^)
}
walk_dir_serial :: proc(dir_path: string, results: ^[dynamic]string) {
has_git := false
entries := read_dir_entries(dir_path, &has_git)
defer free_entries(&entries)
if has_git {
gi := load_gitignore(dir_path)
defer if gi != nil {
destroy(gi)
free(gi)
}
for entry in entries {
if entry.name == ".git" do continue
is_dir := entry.type == .DIR
if gi != nil && is_ignored(gi, entry.name, is_dir) {
if !is_dir {
full_path := join_path(dir_path, entry.name)
append(results, full_path)
}
continue
}
if is_dir {
child_path := join_path(dir_path, entry.name)
walk_dir_serial(child_path, results)
delete(child_path)
}
}
} else {
for entry in entries {
if entry.type == .DIR {
child_path := join_path(dir_path, entry.name)
walk_dir_serial(child_path, results)
delete(child_path)
}
}
}
}
load_gitignore :: proc(dir_path: string) -> ^Gitignore {
gi_path := join_path(dir_path, ".gitignore")
defer delete(gi_path)
gi_path := join_path(dir_path, ".gitignore")
defer delete(gi_path)
data, err := os.read_entire_file_from_path(gi_path, context.allocator)
if err != nil do return nil
data, err := os.read_entire_file_from_path(gi_path, context.allocator)
if err != nil do return nil
gi := new(Gitignore)
gi^ = parse(string(data))
delete(data)
return gi
gi := new(Gitignore)
gi^ = parse(string(data))
delete(data)
return gi
}
join_path :: proc(parent, child: string) -> string {
b: strings.Builder
strings.builder_init(&b)
defer strings.builder_destroy(&b)
b: strings.Builder
strings.builder_init(&b)
defer strings.builder_destroy(&b)
fmt.sbprintf(&b, "%s", parent)
if len(parent) == 0 || parent[len(parent) - 1] != '/' {
fmt.sbprintf(&b, "/")
}
fmt.sbprintf(&b, "%s", child)
fmt.sbprintf(&b, "%s", parent)
if len(parent) == 0 || parent[len(parent) - 1] != '/' {
fmt.sbprintf(&b, "/")
}
fmt.sbprintf(&b, "%s", child)
s := strings.to_string(b)
result, _ := strings.clone(s)
return result
s := strings.to_string(b)
result, _ := strings.clone(s)
return result
}
when FINDR_PARALLEL {
WalkerPool :: struct {
queue: [dynamic]string,
queue_mutex: sync.Mutex,
queue_sema: sync.Atomic_Sema,
results: ^[dynamic]string,
results_mutex: sync.Mutex,
active: i64,
done: sync.One_Shot_Event,
threads: [dynamic]^thread.Thread,
}
walk_parallel :: proc(root: string, results: ^[dynamic]string) {
pool := new(WalkerPool)
pool.queue = make([dynamic]string)
pool.results = results
pool.active = 1
pool.threads = make([dynamic]^thread.Thread)
root_clone, _ := strings.clone(root)
append(&pool.queue, root_clone)
sync.atomic_sema_post(&pool.queue_sema)
num_threads := FINDR_THREADS
for i in 0..<num_threads {
t := thread.create(walk_worker)
t.data = rawptr(pool)
t.init_context = context
thread.start(t)
append(&pool.threads, t)
}
sync.one_shot_event_wait(&pool.done)
for _ in 0..<num_threads {
sync.atomic_sema_post(&pool.queue_sema)
}
for t in pool.threads {
thread.destroy(t)
}
delete(pool.threads)
for path in pool.queue {
delete(path)
}
delete(pool.queue)
free(pool)
}
push_work :: proc(pool: ^WalkerPool, path: string) {
sync.atomic_add_explicit(&pool.active, 1, .Relaxed)
sync.mutex_lock(&pool.queue_mutex)
append(&pool.queue, path)
sync.mutex_unlock(&pool.queue_mutex)
sync.atomic_sema_post(&pool.queue_sema)
}
process_dir_parallel :: proc(pool: ^WalkerPool, dir_path: string) {
has_git := false
entries := read_dir_entries(dir_path, &has_git)
defer free_entries(&entries)
if has_git {
gi := load_gitignore(dir_path)
defer if gi != nil {
destroy(gi)
free(gi)
}
for entry in entries {
if entry.name == ".git" do continue
is_dir := entry.type == .DIR
if gi != nil && is_ignored(gi, entry.name, is_dir) {
if !is_dir {
full_path := join_path(dir_path, entry.name)
sync.mutex_lock(&pool.results_mutex)
append(pool.results, full_path)
sync.mutex_unlock(&pool.results_mutex)
}
continue
}
if is_dir {
child_path := join_path(dir_path, entry.name)
push_work(pool, child_path)
}
}
} else {
for entry in entries {
if entry.type == .DIR {
child_path := join_path(dir_path, entry.name)
push_work(pool, child_path)
}
}
}
}
walk_worker :: proc(t: ^thread.Thread) {
pool := cast(^WalkerPool) t.data
for {
sync.atomic_sema_wait(&pool.queue_sema)
sync.mutex_lock(&pool.queue_mutex)
if len(pool.queue) == 0 {
sync.mutex_unlock(&pool.queue_mutex)
if sync.atomic_load_explicit(&pool.active, .Acquire) == 0 {
sync.one_shot_event_signal(&pool.done)
}
break
}
last := len(pool.queue) - 1
dir_path := pool.queue[last]
ordered_remove(&pool.queue, last)
sync.mutex_unlock(&pool.queue_mutex)
process_dir_parallel(pool, dir_path)
delete(dir_path)
old := sync.atomic_sub_explicit(&pool.active, 1, .Release)
if old == 1 {
sync.one_shot_event_signal(&pool.done)
}
}
}
}