#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <moonbit.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#include <wchar.h>
#else
#include <fcntl.h>
#include <spawn.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#ifdef __APPLE__
#include <Availability.h>
#endif
#endif
#ifndef _WIN32
extern char **environ;
#endif
* moui_cli_write_stderr — write a UTF-8 message followed by a newline to the
* process's stderr stream. Used by the `cli_log.mbt` helpers. The text passed
* in must already include any desired trailing newline; this function adds
* one unconditionally to match `println` semantics.
*/
MOONBIT_FFI_EXPORT void moui_cli_write_stderr(moonbit_bytes_t text) {
if (text != NULL) {
fputs((const char *)text, stderr);
}
fputc('\n', stderr);
fflush(stderr);
}
#ifdef __linux__
#include <fcntl.h>
#include <sys/syscall.h>
#ifndef RENAME_NOREPLACE
#define RENAME_NOREPLACE (1 << 0)
#endif
#endif
#ifdef _WIN32
static wchar_t *moui_cli_utf8_to_wide(const char *value) {
int length = MultiByteToWideChar(
CP_UTF8, MB_ERR_INVALID_CHARS, value, -1, NULL, 0);
if (length <= 0) {
return NULL;
}
wchar_t *wide = (wchar_t *)malloc((size_t)length * sizeof(wchar_t));
if (wide == NULL) {
return NULL;
}
if (MultiByteToWideChar(
CP_UTF8, MB_ERR_INVALID_CHARS, value, -1, wide, length) <= 0) {
free(wide);
return NULL;
}
return wide;
}
static int32_t moui_cli_move_path_windows(
moonbit_bytes_t source,
moonbit_bytes_t destination,
DWORD flags) {
wchar_t *source_wide = moui_cli_utf8_to_wide((const char *)source);
wchar_t *destination_wide =
moui_cli_utf8_to_wide((const char *)destination);
if (source_wide == NULL || destination_wide == NULL) {
free(source_wide);
free(destination_wide);
return -1;
}
BOOL moved = MoveFileExW(source_wide, destination_wide, flags);
free(source_wide);
free(destination_wide);
return moved ? 0 : -1;
}
static wchar_t *moui_cli_final_path_windows(const wchar_t *path) {
HANDLE handle = CreateFileW(
path,
0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
NULL);
if (handle == INVALID_HANDLE_VALUE) {
return NULL;
}
DWORD length = GetFinalPathNameByHandleW(
handle, NULL, 0, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
if (length == 0) {
CloseHandle(handle);
return NULL;
}
wchar_t *result = (wchar_t *)malloc((length + 1) * sizeof(wchar_t));
if (result == NULL) {
CloseHandle(handle);
return NULL;
}
DWORD written = GetFinalPathNameByHandleW(
handle,
result,
length + 1,
FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
CloseHandle(handle);
if (written == 0 || written > length) {
free(result);
return NULL;
}
return result;
}
static moonbit_bytes_t moui_cli_wide_to_utf8_bytes(const wchar_t *value) {
int value_length = (int)wcslen(value);
int length = WideCharToMultiByte(
CP_UTF8,
WC_ERR_INVALID_CHARS,
value,
value_length,
NULL,
0,
NULL,
NULL);
if (length <= 0) {
return moonbit_make_bytes(0, 0);
}
moonbit_bytes_t result = moonbit_make_bytes(length, 0);
if (WideCharToMultiByte(
CP_UTF8,
WC_ERR_INVALID_CHARS,
value,
value_length,
(char *)result,
length,
NULL,
NULL) <= 0) {
return moonbit_make_bytes(0, 0);
}
return result;
}
#endif
MOONBIT_FFI_EXPORT int32_t moui_cli_rename_path(
moonbit_bytes_t source,
moonbit_bytes_t destination) {
#ifdef _WIN32
return moui_cli_move_path_windows(
source, destination, MOVEFILE_WRITE_THROUGH);
#elif defined(__APPLE__)
return renamex_np(
(const char *)source, (const char *)destination, RENAME_EXCL);
#elif defined(__linux__) && defined(SYS_renameat2)
return (int32_t)syscall(
SYS_renameat2,
AT_FDCWD,
(const char *)source,
AT_FDCWD,
(const char *)destination,
RENAME_NOREPLACE);
#else
struct stat status;
if (lstat((const char *)destination, &status) == 0) {
errno = EEXIST;
return -1;
}
if (errno != ENOENT) {
return -1;
}
return rename((const char *)source, (const char *)destination);
#endif
}
MOONBIT_FFI_EXPORT int32_t moui_cli_replace_path(
moonbit_bytes_t source,
moonbit_bytes_t destination) {
#ifdef _WIN32
return moui_cli_move_path_windows(
source,
destination,
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
#else
return rename((const char *)source, (const char *)destination);
#endif
}
MOONBIT_FFI_EXPORT int32_t moui_cli_path_is_executable(
moonbit_bytes_t path) {
#ifdef _WIN32
wchar_t *path_wide = moui_cli_utf8_to_wide((const char *)path);
if (path_wide == NULL) {
return 0;
}
DWORD attributes = GetFileAttributesW(path_wide);
free(path_wide);
return attributes != INVALID_FILE_ATTRIBUTES &&
(attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
#else
return access((const char *)path, X_OK) == 0;
#endif
}
MOONBIT_FFI_EXPORT int32_t moui_cli_path_is_symlink(moonbit_bytes_t path) {
#ifdef _WIN32
wchar_t *path_wide = moui_cli_utf8_to_wide((const char *)path);
if (path_wide == NULL) {
return 0;
}
DWORD attributes = GetFileAttributesW(path_wide);
free(path_wide);
return attributes != INVALID_FILE_ATTRIBUTES &&
(attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
#else
struct stat status;
return lstat((const char *)path, &status) == 0 && S_ISLNK(status.st_mode);
#endif
}
MOONBIT_FFI_EXPORT moonbit_bytes_t moui_cli_canonical_path(
moonbit_bytes_t path) {
#ifdef _WIN32
wchar_t *path_wide = moui_cli_utf8_to_wide((const char *)path);
if (path_wide == NULL) {
return moonbit_make_bytes(0, 0);
}
wchar_t *path_final = moui_cli_final_path_windows(path_wide);
free(path_wide);
if (path_final == NULL) {
return moonbit_make_bytes(0, 0);
}
moonbit_bytes_t result = moui_cli_wide_to_utf8_bytes(path_final);
free(path_final);
return result;
#else
char *path_final = realpath((const char *)path, NULL);
if (path_final == NULL) {
return moonbit_make_bytes(0, 0);
}
size_t length = strlen(path_final);
moonbit_bytes_t result = moonbit_make_bytes((int32_t)length, 0);
memcpy(result, path_final, length);
free(path_final);
return result;
#endif
}
MOONBIT_FFI_EXPORT int32_t moui_cli_path_resolves_within(
moonbit_bytes_t root,
moonbit_bytes_t candidate) {
#ifdef _WIN32
wchar_t *root_wide = moui_cli_utf8_to_wide((const char *)root);
wchar_t *candidate_wide =
moui_cli_utf8_to_wide((const char *)candidate);
if (root_wide == NULL || candidate_wide == NULL) {
free(root_wide);
free(candidate_wide);
return 0;
}
wchar_t *root_final = moui_cli_final_path_windows(root_wide);
wchar_t *candidate_final = moui_cli_final_path_windows(candidate_wide);
free(root_wide);
free(candidate_wide);
if (root_final == NULL || candidate_final == NULL) {
free(root_final);
free(candidate_final);
return 0;
}
size_t root_length = wcslen(root_final);
int inside = _wcsnicmp(root_final, candidate_final, root_length) == 0 &&
(candidate_final[root_length] == L'\0' ||
candidate_final[root_length] == L'\\' ||
candidate_final[root_length] == L'/');
free(root_final);
free(candidate_final);
return inside ? 1 : 0;
#else
char *root_final = realpath((const char *)root, NULL);
char *candidate_final = realpath((const char *)candidate, NULL);
if (root_final == NULL || candidate_final == NULL) {
free(root_final);
free(candidate_final);
return 0;
}
size_t root_length = strlen(root_final);
int inside = strncmp(root_final, candidate_final, root_length) == 0 &&
(candidate_final[root_length] == '\0' ||
candidate_final[root_length] == '/');
free(root_final);
free(candidate_final);
return inside ? 1 : 0;
#endif
}
* The previous `moui_cli_run_doctor_probe` + `moui_cli_doctor_probe_arguments`
* + `moui_cli_doctor_probe_command` C helpers were removed in M10 — the probe
* is now implemented in MoonBit via `process_exec_argv` (see doctor_toolchain.mbt).
*/
#ifdef _WIN32
static int32_t moui_cli_run_validation_command_windows(
const wchar_t *cwd,
HANDLE log_handle,
const wchar_t *command) {
size_t command_length = wcslen(command) + 1;
wchar_t *mutable_command =
(wchar_t *)malloc(command_length * sizeof(wchar_t));
if (mutable_command == NULL) {
return -1;
}
memcpy(mutable_command, command, command_length * sizeof(wchar_t));
STARTUPINFOW startup;
PROCESS_INFORMATION process;
ZeroMemory(&startup, sizeof(startup));
ZeroMemory(&process, sizeof(process));
startup.cb = sizeof(startup);
startup.dwFlags = STARTF_USESTDHANDLES;
startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
startup.hStdOutput = log_handle;
startup.hStdError = log_handle;
BOOL created = CreateProcessW(
NULL,
mutable_command,
NULL,
NULL,
TRUE,
CREATE_NO_WINDOW,
NULL,
cwd,
&startup,
&process);
free(mutable_command);
if (!created) {
return -1;
}
WaitForSingleObject(process.hProcess, INFINITE);
DWORD exit_code = 1;
BOOL read_exit = GetExitCodeProcess(process.hProcess, &exit_code);
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
return read_exit && exit_code == 0 ? 0 : 1;
}
#else
static int moui_cli_spawn_addchdir(
posix_spawn_file_actions_t *actions,
const char *cwd) {
#if defined(__APPLE__) && defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && \
__MAC_OS_X_VERSION_MAX_ALLOWED >= 260000
return posix_spawn_file_actions_addchdir(actions, cwd);
#else
return posix_spawn_file_actions_addchdir_np(actions, cwd);
#endif
}
static int32_t moui_cli_run_validation_command_posix(
const char *cwd,
int log_fd,
char *const argv[]) {
posix_spawn_file_actions_t actions;
if (posix_spawn_file_actions_init(&actions) != 0) {
return -1;
}
if (posix_spawn_file_actions_adddup2(&actions, log_fd, STDOUT_FILENO) != 0 ||
posix_spawn_file_actions_adddup2(&actions, log_fd, STDERR_FILENO) != 0 ||
moui_cli_spawn_addchdir(&actions, cwd) != 0) {
posix_spawn_file_actions_destroy(&actions);
return -1;
}
pid_t child = 0;
int spawn_status = posix_spawnp(
&child, argv[0], &actions, NULL, argv, environ);
posix_spawn_file_actions_destroy(&actions);
if (spawn_status != 0) {
dprintf(log_fd, "could not start moon validation command: %s\n", strerror(spawn_status));
return -1;
}
int status = 0;
while (waitpid(child, &status, 0) < 0) {
if (errno != EINTR) {
return -1;
}
}
if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0)) {
if (WIFEXITED(status)) {
dprintf(log_fd, "moon validation command exited with code %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
dprintf(log_fd, "moon validation command terminated by signal %d\n", WTERMSIG(status));
}
}
return WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : 1;
}
#endif
* Returns 0 on success, 1..3 for the failed validation stage, and 101..103
* when the corresponding process could not be started.
*/
MOONBIT_FFI_EXPORT int32_t moui_cli_validate_generated_project(
moonbit_bytes_t cwd,
moonbit_bytes_t log_path) {
#ifdef _WIN32
wchar_t *cwd_wide = moui_cli_utf8_to_wide((const char *)cwd);
wchar_t *log_wide = moui_cli_utf8_to_wide((const char *)log_path);
if (cwd_wide == NULL || log_wide == NULL) {
free(cwd_wide);
free(log_wide);
return 101;
}
SECURITY_ATTRIBUTES security;
ZeroMemory(&security, sizeof(security));
security.nLength = sizeof(security);
security.bInheritHandle = TRUE;
HANDLE log_handle = CreateFileW(
log_wide,
GENERIC_WRITE,
FILE_SHARE_READ,
&security,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
free(log_wide);
if (log_handle == INVALID_HANDLE_VALUE) {
free(cwd_wide);
return 101;
}
DWORD previous_length =
GetEnvironmentVariableW(L"MOUI_SKIA_DISABLE_PREBUILD_SKIA", NULL, 0);
wchar_t *previous = NULL;
if (previous_length > 0) {
previous = (wchar_t *)malloc(previous_length * sizeof(wchar_t));
if (previous != NULL) {
GetEnvironmentVariableW(
L"MOUI_SKIA_DISABLE_PREBUILD_SKIA", previous, previous_length);
}
}
SetEnvironmentVariableW(L"MOUI_SKIA_DISABLE_PREBUILD_SKIA", L"1");
const wchar_t *commands[] = {
L"moon update",
L"moon check --target all --warn-list +73",
L"moon info --target all",
};
int32_t result = 0;
for (int32_t index = 0; index < 3; ++index) {
int32_t status = moui_cli_run_validation_command_windows(
cwd_wide, log_handle, commands[index]);
if (status != 0) {
result = status < 0 ? 101 + index : 1 + index;
break;
}
}
if (previous != NULL) {
SetEnvironmentVariableW(L"MOUI_SKIA_DISABLE_PREBUILD_SKIA", previous);
} else {
SetEnvironmentVariableW(L"MOUI_SKIA_DISABLE_PREBUILD_SKIA", NULL);
}
free(previous);
CloseHandle(log_handle);
free(cwd_wide);
return result;
#else
int log_fd = open(
(const char *)log_path,
O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR);
if (log_fd < 0) {
return 101;
}
const char *previous_env = getenv("MOUI_SKIA_DISABLE_PREBUILD_SKIA");
char *previous = previous_env == NULL ? NULL : strdup(previous_env);
setenv("MOUI_SKIA_DISABLE_PREBUILD_SKIA", "1", 1);
char *update[] = {(char *)"moon", (char *)"update", NULL};
char *check[] = {
(char *)"moon",
(char *)"check",
(char *)"--target",
(char *)"all",
(char *)"--warn-list",
(char *)"+73",
NULL,
};
char *info[] = {
(char *)"moon", (char *)"info", (char *)"--target", (char *)"all", NULL};
char **commands[] = {update, check, info};
int32_t result = 0;
for (int32_t index = 0; index < 3; ++index) {
int32_t status = moui_cli_run_validation_command_posix(
(const char *)cwd, log_fd, commands[index]);
if (status != 0) {
result = status < 0 ? 101 + index : 1 + index;
break;
}
}
if (previous != NULL) {
setenv("MOUI_SKIA_DISABLE_PREBUILD_SKIA", previous, 1);
} else {
unsetenv("MOUI_SKIA_DISABLE_PREBUILD_SKIA");
}
free(previous);
close(log_fd);
return result;
#endif
}
MOONBIT_FFI_EXPORT int32_t moui_cli_validate_generated_project_stage(
moonbit_bytes_t cwd,
moonbit_bytes_t log_path,
int32_t stage,
int32_t append) {
if (stage < 1 || stage > 3) {
return 100;
}
#ifdef _WIN32
wchar_t *cwd_wide = moui_cli_utf8_to_wide((const char *)cwd);
wchar_t *log_wide = moui_cli_utf8_to_wide((const char *)log_path);
if (cwd_wide == NULL || log_wide == NULL) {
free(cwd_wide);
free(log_wide);
return 100 + stage;
}
SECURITY_ATTRIBUTES security;
ZeroMemory(&security, sizeof(security));
security.nLength = sizeof(security);
security.bInheritHandle = TRUE;
HANDLE log_handle = CreateFileW(
log_wide,
GENERIC_WRITE,
FILE_SHARE_READ,
&security,
append ? OPEN_ALWAYS : CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
free(log_wide);
if (log_handle == INVALID_HANDLE_VALUE) {
free(cwd_wide);
return 100 + stage;
}
if (append) {
SetFilePointer(log_handle, 0, NULL, FILE_END);
}
DWORD previous_length =
GetEnvironmentVariableW(L"MOUI_SKIA_DISABLE_PREBUILD_SKIA", NULL, 0);
wchar_t *previous = NULL;
if (previous_length > 0) {
previous = (wchar_t *)malloc(previous_length * sizeof(wchar_t));
if (previous != NULL) {
GetEnvironmentVariableW(
L"MOUI_SKIA_DISABLE_PREBUILD_SKIA", previous, previous_length);
}
}
SetEnvironmentVariableW(L"MOUI_SKIA_DISABLE_PREBUILD_SKIA", L"1");
const wchar_t *commands[] = {
L"moon update",
L"moon check --target all --warn-list +73",
L"moon info --target all",
};
int32_t status = moui_cli_run_validation_command_windows(
cwd_wide, log_handle, commands[stage - 1]);
if (previous != NULL) {
SetEnvironmentVariableW(L"MOUI_SKIA_DISABLE_PREBUILD_SKIA", previous);
} else {
SetEnvironmentVariableW(L"MOUI_SKIA_DISABLE_PREBUILD_SKIA", NULL);
}
free(previous);
CloseHandle(log_handle);
free(cwd_wide);
return status == 0 ? 0 : (status < 0 ? 100 + stage : stage);
#else
int flags = O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC);
int log_fd = open((const char *)log_path, flags, S_IRUSR | S_IWUSR);
if (log_fd < 0) {
return 100 + stage;
}
const char *previous_env = getenv("MOUI_SKIA_DISABLE_PREBUILD_SKIA");
char *previous = previous_env == NULL ? NULL : strdup(previous_env);
setenv("MOUI_SKIA_DISABLE_PREBUILD_SKIA", "1", 1);
char *update[] = {(char *)"moon", (char *)"update", NULL};
char *check[] = {
(char *)"moon",
(char *)"check",
(char *)"--target",
(char *)"all",
(char *)"--warn-list",
(char *)"+73",
NULL,
};
char *info[] = {
(char *)"moon", (char *)"info", (char *)"--target", (char *)"all", NULL};
char **commands[] = {update, check, info};
int32_t status = moui_cli_run_validation_command_posix(
(const char *)cwd, log_fd, commands[stage - 1]);
if (previous != NULL) {
setenv("MOUI_SKIA_DISABLE_PREBUILD_SKIA", previous, 1);
} else {
unsetenv("MOUI_SKIA_DISABLE_PREBUILD_SKIA");
}
free(previous);
close(log_fd);
return status == 0 ? 0 : (status < 0 ? 100 + stage : stage);
#endif
}