diff --git a/nob.c b/nob.c index acc56b7..6923cc4 100644 --- a/nob.c +++ b/nob.c @@ -21,6 +21,7 @@ const char *test_names[] = { "bytes_for_utf8", "sv_foreach", "svlit_at_different_storages", + "needs_rebuild_locked_file", }; #define test_names_count ARRAY_LEN(test_names) diff --git a/nob.h b/nob.h index 2bdb16b..2d4bf00 100644 --- a/nob.h +++ b/nob.h @@ -2459,41 +2459,34 @@ NOBDEF const char *nob_temp_sv_to_cstr(Nob_String_View sv) NOBDEF int nob_needs_rebuild(const char *output_path, const char **input_paths, size_t input_paths_count) { #ifdef _WIN32 - BOOL bSuccess; - - HANDLE output_path_fd = CreateFile(output_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); - if (output_path_fd == INVALID_HANDLE_VALUE) { + // NOTE: GetFileAttributesEx() reads only the file metadata and never + // opens the file, so it cannot fail just because an editor or an + // antivirus has the file open. CreateFile() with dwShareMode=0 used to + // fail with ERROR_SHARING_VIOLATION in that case, which the POSIX + // stat() implementation below is not susceptible to. + WIN32_FILE_ATTRIBUTE_DATA file_attributes = {0}; + + if (GetFileAttributesExA(output_path, GetFileExInfoStandard, &file_attributes) == 0) { // NOTE: if output does not exist it 100% must be rebuilt - if (GetLastError() == ERROR_FILE_NOT_FOUND) return 1; - nob_log(NOB_ERROR, "Could not open file %s: %s", output_path, nob_win32_error_message(GetLastError())); - return -1; - } - FILETIME output_path_time; - bSuccess = GetFileTime(output_path_fd, NULL, NULL, &output_path_time); - CloseHandle(output_path_fd); - if (!bSuccess) { - nob_log(NOB_ERROR, "Could not get time of %s: %s", output_path, nob_win32_error_message(GetLastError())); + if (GetLastError() == ERROR_FILE_NOT_FOUND) + return 1; + nob_log(NOB_ERROR, "Could not get file attributes of %s: %s", output_path, nob_win32_error_message(GetLastError())); return -1; } + FILETIME output_path_time = file_attributes.ftLastWriteTime; for (size_t i = 0; i < input_paths_count; ++i) { const char *input_path = input_paths[i]; - HANDLE input_path_fd = CreateFile(input_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); - if (input_path_fd == INVALID_HANDLE_VALUE) { + if (GetFileAttributesExA(input_path, GetFileExInfoStandard, &file_attributes) == 0) { // NOTE: non-existing input is an error cause it is needed for building in the first place - nob_log(NOB_ERROR, "Could not open file %s: %s", input_path, nob_win32_error_message(GetLastError())); - return -1; - } - FILETIME input_path_time; - bSuccess = GetFileTime(input_path_fd, NULL, NULL, &input_path_time); - CloseHandle(input_path_fd); - if (!bSuccess) { - nob_log(NOB_ERROR, "Could not get time of %s: %s", input_path, nob_win32_error_message(GetLastError())); + nob_log(NOB_ERROR, "Could not get file attributes of %s: %s", input_path, nob_win32_error_message(GetLastError())); return -1; } + FILETIME input_path_time = file_attributes.ftLastWriteTime; // NOTE: if even a single input_path is fresher than output_path that's 100% rebuild - if (CompareFileTime(&input_path_time, &output_path_time) == 1) return 1; + if (CompareFileTime(&input_path_time, &output_path_time) == 1) + return 1; } return 0; diff --git a/tests/needs_rebuild_locked_file.c b/tests/needs_rebuild_locked_file.c new file mode 100644 index 0000000..4bf51f5 --- /dev/null +++ b/tests/needs_rebuild_locked_file.c @@ -0,0 +1,51 @@ +#include "shared.h" + +#ifdef _WIN32 + #include +#else + #include +#endif + +int main(void) +{ + // The harness runs tests in a fresh temp cwd. + const char *output_path = "output.txt"; + const char *input_path = "input.txt"; + + // Write the output first, then the input, so the input is strictly + // fresher than the output and needs_rebuild() must return 1. + if (!write_entire_file(output_path, "output", 6)) return 1; + + // Make sure the input file gets a strictly fresher timestamp than the + // output file regardless of the filesystem timestamp resolution. +#ifdef _WIN32 + Sleep(100); +#else + struct timespec ts = {0, 100000000}; + nanosleep(&ts, NULL); +#endif + + if (!write_entire_file(input_path, "input", 5)) return 1; + +#ifdef _WIN32 + // Hold the input file the way an editor or an AV scanner would. With + // dwShareMode = 0 nobody else may open it, not even for reading, so + // needs_rebuild() must not rely on opening the file. + HANDLE lock = CreateFileA(input_path, GENERIC_READ, 0, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (lock == INVALID_HANDLE_VALUE) { + fprintf(stderr, "could not lock %s (err=%lu)\n", input_path, GetLastError()); + return 1; + } +#endif // _WIN32 + + // Must NOT fail just because the input file is locked. + int result = needs_rebuild(output_path, &input_path, 1); + printf("needs_rebuild = %d\n", result); + +#ifdef _WIN32 + CloseHandle(lock); +#endif // _WIN32 + + return 0; +} \ No newline at end of file diff --git a/tests/needs_rebuild_locked_file.stdout.txt b/tests/needs_rebuild_locked_file.stdout.txt new file mode 100644 index 0000000..e771de4 --- /dev/null +++ b/tests/needs_rebuild_locked_file.stdout.txt @@ -0,0 +1 @@ +needs_rebuild = 1 diff --git a/tests/needs_rebuild_locked_file.win32.stdout.txt b/tests/needs_rebuild_locked_file.win32.stdout.txt new file mode 100644 index 0000000..e771de4 --- /dev/null +++ b/tests/needs_rebuild_locked_file.win32.stdout.txt @@ -0,0 +1 @@ +needs_rebuild = 1