diff --git a/rakelib/cross.rake b/rakelib/cross.rake index 3de7d79..700a8dd 100644 --- a/rakelib/cross.rake +++ b/rakelib/cross.rake @@ -4,15 +4,14 @@ require 'net/http' require 'rake/clean' require 'open3' require 'etc' +require_relative 'shared' GCC_VERSION = "14.2.0" BINUTILS_VERSION = '2.43.1' GLIBC_VERSION = '2.40' KERNEL_VERSION = '5.15.166' -TMP = Pathname.new('./build') - -CLEAN.include TMP +CLOBBER.include TMP class BuildTarget attr_accessor(:build, :gcc, :target, :tmp) @@ -87,11 +86,16 @@ def download_and_unarchive(url, target) download_and_unarchive URI.parse(response['location']) when Net::HTTPSuccess Open3.popen2 'tar', '-C', target.to_path, archive_type, '-xv' do |stdin, stdout, wait_thread| - stdout.close + Thread.new do + stdout.each { |line| puts line } + end response.read_body do |chunk| stdin.write chunk end + stdin.close + + wait_thread.value end else response.error! @@ -301,7 +305,27 @@ namespace :cross do sh env, 'make', '-j', Etc.nprocessors.to_s, chdir: cwd.to_path sh env, 'make', 'install', chdir: cwd.to_path end + + task :init, [:target] do |_, args| + options = find_build_target GCC_VERSION, args + env = { + 'PATH' => "#{options.rootfs.realpath + 'bin'}:#{ENV['PATH']}" + } + sh env, 'riscv32-unknown-linux-gnu-gcc', + '-ffreestanding', '-static', + '-o', (options.tools + 'init').to_path, + 'tools/init.c' + end end -task cross: ['cross:binutils', 'cross:gcc1', 'cross:headers', 'cross:kernel', 'cross:glibc', 'cross:gcc2'] do +desc 'Build cross toolchain' +task cross: [ + 'cross:binutils', + 'cross:gcc1', + 'cross:headers', + 'cross:kernel', + 'cross:glibc', + 'cross:gcc2', + 'cross:init' +] do end diff --git a/rakelib/shared.rb b/rakelib/shared.rb new file mode 100644 index 0000000..8ecd8cb --- /dev/null +++ b/rakelib/shared.rb @@ -0,0 +1 @@ +TMP = Pathname.new('./build') diff --git a/rakelib/tester.rake b/rakelib/tester.rake new file mode 100644 index 0000000..ef05556 --- /dev/null +++ b/rakelib/tester.rake @@ -0,0 +1,80 @@ +require 'open3' +require 'rake/clean' +require_relative 'shared' + +CLEAN.include(TMP + 'riscv') + +LINKER = 'build/rootfs/riscv32-unknown-linux-gnu/bin/ld' + +namespace :test do + test_sources = FileList['tests/vm/*.elna'] + compiler = `cabal list-bin elna`.strip + object_directory = TMP + 'riscv/tests' + root_directory = TMP + 'riscv/root' + executable_directory = root_directory + 'tests' + expectation_directory = root_directory + 'expectations' + init = TMP + 'riscv/root/init' + + directory root_directory + directory object_directory + directory executable_directory + directory expectation_directory + + test_files = test_sources.flat_map do |test_source| + test_basename = File.basename(test_source, '.elna') + test_object = object_directory + test_basename.ext('.o') + + file test_object => [test_source, object_directory] do + sh compiler, '--output', test_object.to_path, test_source + end + test_executable = executable_directory + test_basename + + file test_executable => [test_object, executable_directory] do + sh LINKER, '-o', test_executable.to_path, test_object.to_path + end + expectation_name = test_basename.ext '.txt' + source_expectation = "tests/expectations/#{expectation_name}" + target_expectation = expectation_directory + expectation_name + + file target_expectation => [source_expectation, expectation_directory] do + cp source_expectation, target_expectation + end + + [test_executable, target_expectation] + end + + file init => [root_directory] do |task| + cp (TMP + 'tools/init'), task.name + end + test_files << init << executable_directory << expectation_directory + + file (TMP + 'riscv/root.cpio') => test_files do |task| + root_files = task.prerequisites + .map { |prerequisite| Pathname.new(prerequisite).relative_path_from(root_directory).to_path } + + File.open task.name, 'wb' do |cpio_file| + cpio_options = { + chdir: root_directory.to_path + } + cpio_stream = Open3.popen2 'cpio', '-o', '--format=newc', cpio_options do |stdin, stdout, wait_thread| + stdin.write root_files.join("\n") + stdin.close + stdout.each { |chunk| cpio_file.write chunk } + wait_thread.value + end + end + end + + task :vm => (TMP + 'riscv/root.cpio') do |task| + kernels = FileList.glob(TMP + 'tools/linux-*/arch/riscv/boot/Image') + + sh 'qemu-system-riscv32', + '-nographic', + '-M', 'virt', + '-bios', 'default', + '-kernel', kernels.first, + '-append', 'quiet panic=1', + '-initrd', task.prerequisites.first, + '-no-reboot' + end +end diff --git a/tests/expectations/empty.txt b/tests/expectations/empty.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/vm/empty.elna b/tests/vm/empty.elna new file mode 100644 index 0000000..e69de29 diff --git a/tools/init.c b/tools/init.c new file mode 100644 index 0000000..cb646bd --- /dev/null +++ b/tools/init.c @@ -0,0 +1,205 @@ +#include +#include +#include +#include +#include +#include +#include + +#define FILENAME_BUFFER_SIZE 256 + +size_t read_command(int descriptor, char *command_buffer) +{ + ssize_t bytes_read = 0; + size_t read_so_far = 0; + + while ((bytes_read = read(descriptor, command_buffer + read_so_far, FILENAME_BUFFER_SIZE - read_so_far - 1)) > 0) + { + read_so_far += bytes_read; + if (read_so_far >= FILENAME_BUFFER_SIZE - 1) + { + break; + } + } + command_buffer[read_so_far] = 0; + return read_so_far; +} + +enum status +{ + status_success, + status_failure, + status_warning, + status_fatal +}; + +unsigned int make_path(char *destination, const char *directory, const char *filename, const char *extension) +{ + unsigned int i = 0; + + for (; i < FILENAME_BUFFER_SIZE; i++) + { + if (directory[i] == 0) + { + break; + } + destination[i] = directory[i]; + } + for (int j = 0; i < FILENAME_BUFFER_SIZE; i++, j++) + { + if (filename[j] == 0) + { + break; + } + destination[i] = filename[j]; + } + if (extension == NULL) + { + goto done; + } + for (int j = 0; i < FILENAME_BUFFER_SIZE; i++, j++) + { + if (extension[j] == 0) + { + break; + } + destination[i] = extension[j]; + } +done: + destination[i] = 0; + + return i; +} + +enum status run_test(const char *file_entry_name) +{ + printf("Running %s. ", file_entry_name); + + char filename[FILENAME_BUFFER_SIZE]; + char command_buffer[FILENAME_BUFFER_SIZE]; + char file_buffer[256]; + int pipe_ends[2]; + + if (pipe(pipe_ends) == -1) + { + perror("pipe"); + return status_fatal; + } + make_path(filename, "./tests/", file_entry_name, NULL); + + int child_pid = fork(); + if (child_pid == -1) + { + return status_fatal; + } + else if (child_pid == 0) + { + close(STDIN_FILENO); + close(STDERR_FILENO); + close(pipe_ends[0]); // Close the read end. + + if (dup2(pipe_ends[1], STDOUT_FILENO) == -1) + { + perror("dup2"); + } + else + { + execl(filename, filename); + perror("execl"); + } + close(STDOUT_FILENO); + close(pipe_ends[1]); + _exit(1); + } + else + { + close(pipe_ends[1]); // Close the write end. + read_command(pipe_ends[0], command_buffer); + close(pipe_ends[0]); + + int wait_status = 0; + wait(&wait_status); + + make_path(filename, "./expectations/", file_entry_name, ".txt"); + + FILE *expectation_descriptor = fopen(filename, "r"); + + if (expectation_descriptor == NULL) + { + return status_warning; + } + size_t read_from_file = fread(file_buffer, 1, sizeof(file_buffer) - 1, expectation_descriptor); + fclose(expectation_descriptor); + + file_buffer[read_from_file] = 0; + for (unsigned int i = 0; ; ++i) + { + if (command_buffer[i] == 0 && file_buffer[i] == 0) + { + fwrite("\n", 1, 1, stdout); + return status_success; + } + else if (command_buffer[i] != file_buffer[i]) + { + printf("Failed. Got:\n%s", command_buffer); + return status_failure; + } + } + } +} + +struct summary +{ + size_t total; + size_t failure; + size_t success; +}; + +void walk() +{ + DIR *directory_stream = opendir("./tests"); + struct dirent *file_entry; + + struct summary test_summary = { .total = 0, .failure = 0, .success = 0 }; + + while ((file_entry = readdir(directory_stream)) != NULL) + { + if (file_entry->d_name[0] == '.') + { + continue; + } + ++test_summary.total; + switch (run_test(file_entry->d_name)) + { + case status_failure: + ++test_summary.failure; + break; + case status_success: + ++test_summary.success; + break; + case status_warning: + break; + case status_fatal: + goto end_walk; + } + } + printf("Successful: %lu, Failed: %lu, Total: %lu.\n", + test_summary.success, test_summary.failure, test_summary.total); +end_walk: + closedir(directory_stream); +} + +int main() +{ + int dev_console = open("/dev/console", O_WRONLY); + if (dev_console != -1) + { + dup2(dev_console, STDOUT_FILENO); + walk(); + close(dev_console); + } + sync(); + reboot(RB_POWER_OFF); + + return 1; +}