diff --git a/README.md b/README.md index 524d0235c7..2e29f9b497 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ compromises backward compatibility. * Release mode produces heavily optimized code. What other projects call "Link Time Optimization" Zig does automatically. * Mark functions as tests and automatically run them with `zig test`. - * Currently supported architectures: `x86_64`, `i386` - * Currently supported operating systems: linux, macosx + * Currently supported architectures: `x86_64` + * Currently supported operating systems: linux * Friendly toward package maintainers. Reproducible build, bootstrapping process carefully documented. Issues filed by package maintainers are considered especially important. @@ -103,7 +103,7 @@ cd build cmake .. -DCMAKE_INSTALL_PREFIX=$(pwd) -DZIG_LIBC_LIB_DIR=$(dirname $(cc -print-file-name=crt1.o)) -DZIG_LIBC_INCLUDE_DIR=$(echo -n | cc -E -x c - -v 2>&1 | grep -B1 "End of search list." | head -n1 | cut -c 2- | sed "s/ .*//") -DZIG_LIBC_STATIC_LIB_DIR=$(dirname $(cc -print-file-name=crtbegin.o)) make make install -./run_tests +./zig build --build-file ../build.zig test ``` ### Release / Install Build diff --git a/build.zig b/build.zig index 918a629cf6..378ea66c86 100644 --- a/build.zig +++ b/build.zig @@ -5,44 +5,19 @@ pub fn build(b: &Builder) { const test_filter = b.option([]const u8, "test-filter", "Skip tests that do not match filter"); const test_step = b.step("test", "Run all the tests"); - const behavior_tests = b.step("test-behavior", "Run the behavior tests"); - test_step.dependOn(behavior_tests); - for ([]bool{false, true}) |release| { - for ([]bool{false, true}) |link_libc| { - const these_tests = b.addTest("test/behavior.zig"); - these_tests.setNamePrefix(b.fmt("behavior-{}-{} ", - if (release) "release" else "debug", - if (link_libc) "c" else "bare")); - these_tests.setFilter(test_filter); - these_tests.setRelease(release); - if (link_libc) { - these_tests.linkLibrary("c"); - } - behavior_tests.dependOn(&these_tests.step); - } - } + const cleanup = b.addRemoveDirTree("test_artifacts"); + test_step.dependOn(&cleanup.step); - const std_lib_tests = b.step("test-std", "Run the standard library tests"); - test_step.dependOn(std_lib_tests); - for ([]bool{false, true}) |release| { - for ([]bool{false, true}) |link_libc| { - const these_tests = b.addTest("std/index.zig"); - these_tests.setNamePrefix(b.fmt("std-{}-{} ", - if (release) "release" else "debug", - if (link_libc) "c" else "bare")); - these_tests.setFilter(test_filter); - these_tests.setRelease(release); - if (link_libc) { - these_tests.linkLibrary("c"); - } - std_lib_tests.dependOn(&these_tests.step); - } - } + cleanup.step.dependOn(tests.addPkgTests(b, test_filter, + "test/behavior.zig", "behavior", "Run the behavior tests")); - test_step.dependOn(tests.addCompareOutputTests(b, test_filter)); - test_step.dependOn(tests.addBuildExampleTests(b, test_filter)); - test_step.dependOn(tests.addCompileErrorTests(b, test_filter)); - test_step.dependOn(tests.addAssembleAndLinkTests(b, test_filter)); - test_step.dependOn(tests.addDebugSafetyTests(b, test_filter)); - test_step.dependOn(tests.addParseHTests(b, test_filter)); + cleanup.step.dependOn(tests.addPkgTests(b, test_filter, + "std/index.zig", "std", "Run the standard library tests")); + + cleanup.step.dependOn(tests.addCompareOutputTests(b, test_filter)); + cleanup.step.dependOn(tests.addBuildExampleTests(b, test_filter)); + cleanup.step.dependOn(tests.addCompileErrorTests(b, test_filter)); + cleanup.step.dependOn(tests.addAssembleAndLinkTests(b, test_filter)); + cleanup.step.dependOn(tests.addDebugSafetyTests(b, test_filter)); + cleanup.step.dependOn(tests.addParseHTests(b, test_filter)); } diff --git a/std/build.zig b/std/build.zig index 20fb4cad5c..d22d6b660a 100644 --- a/std/build.zig +++ b/std/build.zig @@ -195,6 +195,12 @@ pub const Builder = struct { return log_step; } + pub fn addRemoveDirTree(self: &Builder, dir_path: []const u8) -> &RemoveDirStep { + const remove_dir_step = %%self.allocator.create(RemoveDirStep); + *remove_dir_step = RemoveDirStep.init(self, dir_path); + return remove_dir_step; + } + pub fn version(self: &const Builder, major: u32, minor: u32, patch: u32) -> Version { Version { .major = major, @@ -1548,10 +1554,12 @@ pub const WriteFileStep = struct { const full_path = self.builder.pathFromRoot(self.file_path); const full_path_dir = %%os.path.dirname(self.builder.allocator, full_path); os.makePath(self.builder.allocator, full_path_dir) %% |err| { - debug.panic("unable to make path {}: {}\n", full_path_dir, @errorName(err)); + %%io.stderr.printf("unable to make path {}: {}\n", full_path_dir, @errorName(err)); + return err; }; io.writeFile(full_path, self.data, self.builder.allocator) %% |err| { - debug.panic("unable to write {}: {}\n", full_path, @errorName(err)); + %%io.stderr.printf("unable to write {}: {}\n", full_path, @errorName(err)); + return err; }; } }; @@ -1576,6 +1584,30 @@ pub const LogStep = struct { } }; +pub const RemoveDirStep = struct { + step: Step, + builder: &Builder, + dir_path: []const u8, + + pub fn init(builder: &Builder, dir_path: []const u8) -> RemoveDirStep { + return RemoveDirStep { + .builder = builder, + .step = Step.init(builder.fmt("RemoveDir {}", dir_path), builder.allocator, make), + .dir_path = dir_path, + }; + } + + fn make(step: &Step) -> %void { + const self = @fieldParentPtr(RemoveDirStep, "step", step); + + const full_path = self.builder.pathFromRoot(self.dir_path); + os.deleteTree(self.builder.allocator, full_path) %% |err| { + %%io.stderr.printf("Unable to remove {}: {}\n", full_path, @errorName(err)); + return err; + }; + } +}; + pub const Step = struct { name: []const u8, makeFn: fn(self: &Step) -> %void, diff --git a/std/io.zig b/std/io.zig index 03960867b5..e9c70d9611 100644 --- a/std/io.zig +++ b/std/io.zig @@ -57,8 +57,6 @@ error NoMem; error Unseekable; error Eof; -const buffer_size = 4 * 1024; - pub const OpenRead = 0b0001; pub const OpenWrite = 0b0010; pub const OpenCreate = 0b0100; @@ -66,7 +64,7 @@ pub const OpenTruncate = 0b1000; pub const OutStream = struct { fd: i32, - buffer: [buffer_size]u8, + buffer: [os.page_size]u8, index: usize, /// `path` may need to be copied in memory to add a null terminating byte. In this case @@ -97,7 +95,7 @@ pub const OutStream = struct { } pub fn write(self: &OutStream, bytes: []const u8) -> %void { - if (bytes.len >= buffer_size) { + if (bytes.len >= self.buffer.len) { %return self.flush(); return os.posixWrite(self.fd, bytes); } @@ -329,7 +327,7 @@ pub const InStream = struct { } pub fn readAll(is: &InStream, buf: &Buffer0) -> %void { - %return buf.resize(buffer_size); + %return buf.resize(os.page_size); var actual_buf_len: usize = 0; while (true) { @@ -341,7 +339,7 @@ pub const InStream = struct { return buf.resize(actual_buf_len); } - %return buf.resize(actual_buf_len + buffer_size); + %return buf.resize(actual_buf_len + os.page_size); } } }; diff --git a/std/list.zig b/std/list.zig index d01e3a88a4..b20cd06fef 100644 --- a/std/list.zig +++ b/std/list.zig @@ -61,10 +61,15 @@ pub fn List(comptime T: type) -> type{ l.len = new_length; return result; } + + pub fn pop(self: &Self) -> T { + self.len -= 1; + return self.items[self.len]; + } } } -test "basicListTest" { +test "basic list test" { var list = List(i32).init(&debug.global_allocator); defer list.deinit(); @@ -75,4 +80,7 @@ test "basicListTest" { {var i: usize = 0; while (i < 10; i += 1) { assert(list.items[i] == i32(i + 1)); }} + + assert(list.pop() == 10); + assert(list.len == 9); } diff --git a/std/mem.zig b/std/mem.zig index 66e119b503..6a2d854fc7 100644 --- a/std/mem.zig +++ b/std/mem.zig @@ -9,6 +9,8 @@ error NoMem; pub const Allocator = struct { allocFn: fn (self: &Allocator, n: usize) -> %[]u8, + /// Note that old_mem may be a slice of length 0, in which case reallocFn + /// should simply call allocFn reallocFn: fn (self: &Allocator, old_mem: []u8, new_size: usize) -> %[]u8, freeFn: fn (self: &Allocator, mem: []u8), diff --git a/std/os/index.zig b/std/os/index.zig index 7da623d3ce..0113706796 100644 --- a/std/os/index.zig +++ b/std/os/index.zig @@ -17,6 +17,8 @@ pub const line_sep = switch (@compileVar("os")) { else => "\n", }; +pub const page_size = 4 * 1024; + const debug = @import("../debug.zig"); const assert = debug.assert; @@ -32,6 +34,7 @@ const cstr = @import("../cstr.zig"); const io = @import("../io.zig"); const base64 = @import("../base64.zig"); +const List = @import("../list.zig").List; error Unexpected; error SystemResources; @@ -46,6 +49,7 @@ error SymLinkLoop; error ReadOnlyFileSystem; error LinkQuotaExceeded; error RenameAcrossMountPoints; +error DirNotEmpty; /// Fills `buf` with random bytes. If linking against libc, this calls the /// appropriate OS-specific library call. Otherwise it uses the zig standard @@ -585,3 +589,177 @@ pub fn makePath(allocator: &Allocator, full_path: []const u8) -> %void { // TODO stat the file and return an error if it's not a directory }; } + +/// Returns ::error.DirNotEmpty if the directory is not empty. +/// To delete a directory recursively, see ::deleteTree +pub fn deleteDir(allocator: &Allocator, dir_path: []const u8) -> %void { + const path_buf = %return allocator.alloc(u8, dir_path.len + 1); + defer allocator.free(path_buf); + + mem.copy(u8, path_buf, dir_path); + path_buf[dir_path.len] = 0; + + const err = posix.getErrno(posix.rmdir(path_buf.ptr)); + if (err > 0) { + return switch (err) { + errno.EACCES, errno.EPERM => error.AccessDenied, + errno.EBUSY => error.FileBusy, + errno.EFAULT, errno.EINVAL => unreachable, + errno.ELOOP => error.SymLinkLoop, + errno.ENAMETOOLONG => error.NameTooLong, + errno.ENOENT => error.FileNotFound, + errno.ENOMEM => error.SystemResources, + errno.ENOTDIR => error.NotDir, + errno.EEXIST, errno.ENOTEMPTY => error.DirNotEmpty, + errno.EROFS => error.ReadOnlyFileSystem, + else => error.Unexpected, + }; + } +} + +/// Whether ::full_path describes a symlink, file, or directory, this function +/// removes it. If it cannot be removed because it is a non-empty directory, +/// this function recursively removes its entries and then tries again. +// TODO non-recursive implementation +pub fn deleteTree(allocator: &Allocator, full_path: []const u8) -> %void { +start_over: + // First, try deleting the item as a file. This way we don't follow sym links. + try (deleteFile(allocator, full_path)) { + return; + } else |err| { + if (err == error.FileNotFound) + return; + if (err != error.IsDir) + return err; + } + { + var dir = Dir.open(allocator, full_path) %% |err| { + if (err == error.FileNotFound) + return; + if (err == error.NotDir) + goto start_over; + return err; + }; + defer dir.close(); + + var full_entry_buf = List(u8).init(allocator); + defer full_entry_buf.deinit(); + + while (true) { + const entry = (%return dir.next()) ?? break; + + %return full_entry_buf.resize(full_path.len + entry.name.len + 1); + const full_entry_path = full_entry_buf.toSlice(); + mem.copy(u8, full_entry_path, full_path); + full_entry_path[full_path.len] = '/'; + mem.copy(u8, full_entry_path[full_path.len + 1...], entry.name); + + %return deleteTree(allocator, full_entry_path); + } + } + return deleteDir(allocator, full_path); +} + +pub const Dir = struct { + fd: i32, + allocator: &Allocator, + buf: []u8, + index: usize, + end_index: usize, + + const LinuxEntry = extern struct { + d_ino: usize, + d_off: usize, + d_reclen: u16, + d_name: u8, // field address is the address of first byte of name + }; + + pub const Entry = struct { + name: []const u8, + kind: Kind, + + pub const Kind = enum { + BlockDevice, + CharacterDevice, + Directory, + NamedPipe, + SymLink, + File, + UnixDomainSocket, + Unknown, + }; + }; + + pub fn open(allocator: &Allocator, dir_path: []const u8) -> %Dir { + const fd = %return posixOpen(dir_path, posix.O_RDONLY|posix.O_DIRECTORY|posix.O_CLOEXEC, 0, allocator); + return Dir { + .allocator = allocator, + .fd = fd, + .index = 0, + .end_index = 0, + .buf = []u8{}, + }; + } + + pub fn close(self: &Dir) { + self.allocator.free(self.buf); + posixClose(self.fd); + } + + /// Memory such as file names referenced in this returned entry becomes invalid + /// with subsequent calls to next, as well as when this ::Dir is deinitialized. + pub fn next(self: &Dir) -> %?Entry { + start_over: + if (self.index >= self.end_index) { + if (self.buf.len == 0) { + self.buf = %return self.allocator.alloc(u8, 2); //page_size); + } + + while (true) { + const result = posix.getdents(self.fd, self.buf.ptr, self.buf.len); + const err = linux.getErrno(result); + if (err > 0) { + switch (err) { + errno.EBADF, errno.EFAULT, errno.ENOTDIR => unreachable, + errno.EINVAL => { + self.buf = %return self.allocator.realloc(u8, self.buf, self.buf.len * 2); + continue; + }, + else => return error.Unexpected, + }; + } + if (result == 0) + return null; + self.index = 0; + self.end_index = result; + break; + } + } + const linux_entry = @ptrcast(&LinuxEntry, &self.buf[self.index]); + const next_index = self.index + linux_entry.d_reclen; + self.index = next_index; + + const name = (&linux_entry.d_name)[0...cstr.len(&linux_entry.d_name)]; + + // skip . and .. entries + if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { + goto start_over; + } + + const type_char = self.buf[next_index - 1]; + const entry_kind = switch (type_char) { + posix.DT_BLK => Entry.Kind.BlockDevice, + posix.DT_CHR => Entry.Kind.CharacterDevice, + posix.DT_DIR => Entry.Kind.Directory, + posix.DT_FIFO => Entry.Kind.NamedPipe, + posix.DT_LNK => Entry.Kind.SymLink, + posix.DT_REG => Entry.Kind.File, + posix.DT_SOCK => Entry.Kind.UnixDomainSocket, + else => Entry.Kind.Unknown, + }; + return Entry { + .name = name, + .kind = entry_kind, + }; + } +}; diff --git a/std/os/linux.zig b/std/os/linux.zig index 7c841fafad..1ebade3baf 100644 --- a/std/os/linux.zig +++ b/std/os/linux.zig @@ -241,6 +241,15 @@ pub const AF_NFC = PF_NFC; pub const AF_VSOCK = PF_VSOCK; pub const AF_MAX = PF_MAX; +pub const DT_UNKNOWN = 0; +pub const DT_FIFO = 1; +pub const DT_CHR = 2; +pub const DT_DIR = 4; +pub const DT_BLK = 6; +pub const DT_REG = 8; +pub const DT_LNK = 10; +pub const DT_SOCK = 12; +pub const DT_WHT = 14; fn unsigned(s: i32) -> u32 { *@ptrcast(&u32, &s) } fn signed(s: u32) -> i32 { *@ptrcast(&i32, &s) } @@ -273,6 +282,10 @@ pub fn getcwd(buf: &u8, size: usize) -> usize { arch.syscall2(arch.SYS_getcwd, usize(buf), size) } +pub fn getdents(fd: i32, dirp: &u8, count: usize) -> usize { + arch.syscall3(arch.SYS_getdents, usize(fd), usize(dirp), usize(count)) +} + pub fn mkdir(path: &const u8, mode: usize) -> usize { arch.syscall2(arch.SYS_mkdir, usize(path), mode) } @@ -291,6 +304,10 @@ pub fn read(fd: i32, buf: &u8, count: usize) -> usize { arch.syscall3(arch.SYS_read, usize(fd), usize(buf), count) } +pub fn rmdir(path: &const u8) -> usize { + arch.syscall1(arch.SYS_rmdir, usize(path)) +} + pub fn symlink(existing: &const u8, new: &const u8) -> usize { arch.syscall2(arch.SYS_symlink, usize(existing), usize(new)) } diff --git a/test/tests.zig b/test/tests.zig index 88e2231304..d760856339 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -103,6 +103,27 @@ pub fn addParseHTests(b: &build.Builder, test_filter: ?[]const u8) -> &build.Ste return cases.step; } +pub fn addPkgTests(b: &build.Builder, test_filter: ?[]const u8, root_src: []const u8, + name:[] const u8, desc: []const u8) -> &build.Step +{ + const step = b.step(b.fmt("test-{}", name), desc); + for ([]bool{false, true}) |release| { + for ([]bool{false, true}) |link_libc| { + const these_tests = b.addTest(root_src); + these_tests.setNamePrefix(b.fmt("{}-{}-{} ", name, + if (release) "release" else "debug", + if (link_libc) "c" else "bare")); + these_tests.setFilter(test_filter); + these_tests.setRelease(release); + if (link_libc) { + these_tests.linkLibrary("c"); + } + step.dependOn(&these_tests.step); + } + } + return step; +} + pub const CompareOutputContext = struct { b: &build.Builder, step: &build.Step,