Skip to content

Commit

Permalink
MacOS support (#382)
Browse files Browse the repository at this point in the history
  • Loading branch information
Akuli authored Dec 4, 2023
1 parent 5132347 commit 171226c
Show file tree
Hide file tree
Showing 14 changed files with 243 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: shellcheck --color=always --shell=bash --exclude=SC2086,SC2059,SC2046,SC2235,SC2002,SC2206,SC2068 *.sh activate
- run: shellcheck --color=always --shell=bash --exclude=SC2086,SC2059,SC2046,SC2235,SC2002,SC2206,SC2068,SC2207 *.sh activate

test:
runs-on: ubuntu-latest
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
on:
push:
branches:
- main
pull_request:

jobs:
test:
runs-on: macos-latest
strategy:
matrix:
# Testing all levels because there was a bug that only happened with -O1. (#224)
opt-level: ['-O0', '-O1', '-O2', '-O3']
steps:
- uses: actions/checkout@v3
- run: brew install bash diffutils llvm@13
- run: make
- run: ./runtests.sh --verbose --jou-flags "${{ matrix.opt-level }}"
- run: ./runtests.sh --verbose --jou-flags "${{ matrix.opt-level }} --verbose"
- run: make clean
- name: Check that "make clean" deleted all files not committed to Git
shell: bash
run: |
if [ "$(git status --porcelain --ignored)" != "" ]; then
git status --ignored
exit 1
fi
compare-compilers:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- run: brew install bash diffutils llvm@13
- run: ./compare_compilers.sh
11 changes: 9 additions & 2 deletions Makefile.posix
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
LLVM_CONFIG ?= $(shell which llvm-config-13 || which llvm-config-11)
# Linux has llvm-config-13, on macos brew installs LLVM to a weird place in /usr/local/
LLVM_CONFIG ?= $(shell \
which llvm-config-13 \
|| which /usr/local/opt/llvm@13/bin/llvm-config \
|| which llvm-config-11 \
|| which /usr/local/opt/llvm@11/bin/llvm-config \
)
CFLAGS += $(shell $(LLVM_CONFIG) --cflags)
LDFLAGS ?= $(shell $(LLVM_CONFIG) --ldflags --libs)

Expand All @@ -14,6 +20,7 @@ compile_flags.txt:
echo "-I$(shell $(LLVM_CONFIG) --includedir)" > compile_flags.txt

config.h:
@v=`$(LLVM_CONFIG) --version`; case "$$v" in 11.*|13.*) ;; *) echo "Error: Found unsupported LLVM version $$v. Only LLVM 11 and LLVM 13 are supported."; exit 1; esac
echo "// auto-generated by Makefile" > config.h
echo "#define JOU_CLANG_PATH \"$(shell $(LLVM_CONFIG) --bindir)/clang\"" >> config.h

Expand All @@ -34,4 +41,4 @@ self_hosted_compiler: jou config.jou $(wildcard self_hosted/*.jou)
.PHONY: clean
clean:
rm -rvf obj jou jou.exe self_hosted_compiler self_hosted_compiler.exe tmp config.h config.jou compile_flags.txt
find -name jou_compiled -print -exec rm -rf '{}' +
find . -name jou_compiled -print -exec rm -rf '{}' +
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,46 @@ $ LLVM_CONFIG=llvm-config-11 make
</details>
<details> <summary>MacOS</summary>
MacOS support is new. Please create an issue if something doesn't work.
1. Install Git, make and LLVM 13.
If you do software development on MacOS, you probably already have Git and make,
because they come with Xcode Command Line Tools.
You can use [brew](https://brew.sh/) to install LLVM 13:
```
$ brew install llvm@13
```
2. Download and compile Jou.
```
$ git clone https://github.com/Akuli/jou
$ cd jou
$ make
```
3. Run the hello world program to make sure that Jou works:
```
$ ./jou examples/hello.jou
Hello World
```
You can now run other Jou programs in the same way.
4. (Optional) If you want to run Jou programs with simply `jou filename`
instead of something like `./jou filename` or `/full/path/to/jou filename`,
you can add the `jou` directory to your PATH.
To do so, edit `~/.bashrc` (or whatever other file you have instead, e.g. `~/.zshrc`):
```
$ nano ~/.bashrc
```
Add the following line to the end:
```
export PATH="$PATH:/Users/yourname/jou/"
```
Replace `/Users/yourname/jou/` with the path to the folder (not the executable file) where you downloaded Jou.
Note that the `~` character does not work here,
so you need to use a full path (or `$HOME`) instead.
</details>
<details> <summary>64-bit Windows</summary>
1. Go to releases on GitHub. It's in the sidebar at right.
Expand Down
4 changes: 2 additions & 2 deletions compare_compilers.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
# There are two Jou compilers: one written in C and another written in Jou.
# They should be able to tokenize and parse each Jou file in exactly the same way.
Expand Down Expand Up @@ -29,7 +29,7 @@ done

if [ ${#files[@]} = 0 ]; then
# TODO: do not skip Advent Of Code files
mapfile -t files < <( find stdlib examples tests -name '*.jou' | grep -v aoc2023 | sort )
files=( $(find stdlib examples tests -name '*.jou' | grep -v aoc2023 | sort) )
fi
if [ ${#actions[@]} = 0 ]; then
actions=(tokenize parse run)
Expand Down
7 changes: 4 additions & 3 deletions runtests.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
#
#
Expand Down Expand Up @@ -119,8 +119,9 @@ function post_process_output()
echo "A lot of output hidden..."
grep "^Exit code:"
elif [[ $joufile =~ ^tests/crash/ ]]; then
if [[ "$OS" =~ Windows ]]; then
# Windows doesn't say "Segmentation fault" when a program crashes
if [ "$(uname -s)" != Linux ]; then
# Windows and macos don't seem to say "Segmentation fault" when
# a program crashes
echo "Segmentation fault"
fi
# Hide most of the output. We really only care about whether it
Expand Down
70 changes: 61 additions & 9 deletions self_hosted/paths.jou
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,83 @@ declare _mkdir(path: byte*) -> int # windows
declare mkdir(path: byte*, mode: int) -> int # posix
declare dirname(path: byte*) -> byte*
declare stat(path: byte*, buf: byte[1000]*) -> int # lol
declare popen(command: byte*, type: byte*) -> FILE*
declare pclose(stream: FILE*) -> int
declare _NSGetExecutablePath(buf: byte*, bufsize: int*) -> int


def is_windows() -> bool:
# TODO: this is just weird...
return getenv("OS") != NULL and strstr(getenv("OS"), "Windows") != NULL

# Return a path to the currently running program. Return value must be free()d.
def find_current_executable() -> byte*:
# https://stackoverflow.com/a/3466183
def is_macos() -> bool:
if is_windows():
return False

uname = popen("uname", "r")
if uname == NULL:
return False

output: byte[100]
memset(&output, 0, sizeof(output))
fgets(output, sizeof(output) as int, uname)

pclose(uname)
return starts_with(output, "Darwin")


def _find_current_executable_windows() -> byte*:
buf = NULL
for size = 2L; True; size *= 2:
buf = realloc(buf, size)
memset(buf, 0, size)
ret = GetModuleFileNameA(NULL, buf, size as int)
if ret <= 0:
return NULL # error --> give up
if ret < size:
# buffer is big enough, it fits
return buf

if is_windows():
ret: long = GetModuleFileNameA(NULL, buf, size as int)
else:
ret = readlink("/proc/self/exe", buf, size)
def _find_current_executable_macos() -> byte*:
n = 1
result: byte* = malloc(n)
ret = _NSGetExecutablePath(result, &n) # sets n to desired size
assert ret < 0 # didn't fit
result = realloc(result, n)
ret = _NSGetExecutablePath(result, &n)
assert ret == 0
return result

def _find_current_executable_linux() -> byte*:
buf = NULL
for size = 2L; True; size *= 2:
buf = realloc(buf, size)
memset(buf, 0, size)
ret = readlink("/proc/self/exe", buf, size)
if ret <= 0:
# TODO: include os error message (GetLastError / errno)
fprintf(stderr, "error: cannot locate currently running executable, needed for finding the Jou standard library\n")
exit(1)
return NULL
if ret < size:
# buffer is big enough, it fits
return buf


def find_current_executable() -> byte*:
if is_windows():
result = _find_current_executable_windows()
elif is_macos():
result = _find_current_executable_macos()
else:
result = _find_current_executable_linux()

if result == NULL:
# TODO: include os error message (GetLastError / errno)
fprintf(stderr, "error: cannot locate currently running executable, needed for finding the Jou standard library\n")
exit(1)

return result


def find_installation_directory() -> byte*:
exe = find_current_executable()
result = strdup(dirname(exe))
Expand Down
1 change: 1 addition & 0 deletions self_hosted/runs_wrong.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ tests/wrong_type/cannot_be_indexed.jou
tests/wrong_type/index.jou
stdlib/ascii.jou
tests/should_succeed/ascii_test.jou
stdlib/_macos_startup.jou
19 changes: 14 additions & 5 deletions src/codegen.c
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,11 @@ static LLVMValueRef codegen_function_or_method_decl(const struct State *st, cons
// make it a definition instead of a declaration so that there are no linker errors.
// Ideally it would be possible to compile some parts of Jou code only for a specific platform.
#ifdef _WIN32
const char *doesnt_exist[] = { "readlink", "mkdir" };
#else
const char *doesnt_exist[] = { "readlink", "mkdir", "popen", "pclose", "_NSGetExecutablePath" };
#elif defined(__APPLE__)
const char *doesnt_exist[] = { "GetModuleFileNameA", "_mkdir" };
#else
const char *doesnt_exist[] = { "GetModuleFileNameA", "_mkdir", "_NSGetExecutablePath" };
#endif
for (unsigned i = 0; i < sizeof doesnt_exist / sizeof doesnt_exist[0]; i++) {
if (!strcmp(fullname, doesnt_exist[i])) {
Expand Down Expand Up @@ -434,11 +436,18 @@ static int find_block(const CfGraph *cfg, const CfBlock *b)
assert(0);
}

#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
static void codegen_call_to_the_special_startup_function(const struct State *st)
{
const char *name;
#ifdef _WIN32
name = "_jou_windows_startup";
#else
name = "_jou_macos_startup";
#endif

LLVMTypeRef functype = LLVMFunctionType(LLVMVoidType(), NULL, 0, false);
LLVMValueRef func = LLVMAddFunction(st->module, "_jou_windows_startup", functype);
LLVMValueRef func = LLVMAddFunction(st->module, name, functype);
LLVMBuildCall2(st->builder, functype, func, NULL, 0, "");
}
#endif
Expand All @@ -461,7 +470,7 @@ static void codegen_function_or_method_def(struct State *st, const CfGraph *cfg)
assert(cfg->all_blocks.ptr[0] == &cfg->start_block);
LLVMPositionBuilderAtEnd(st->builder, blocks[0]);

#ifdef _WIN32
#if defined(_WIN32) || defined(__APPLE__)
if (!get_self_class(&cfg->signature) && !strcmp(cfg->signature.name, "main"))
codegen_call_to_the_special_startup_function(st);
#endif
Expand Down
3 changes: 3 additions & 0 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,9 @@ int main(int argc, char **argv)
#ifdef _WIN32
include_special_stdlib_file(&compst, "_windows_startup.jou");
#endif
#ifdef __APPLE__
include_special_stdlib_file(&compst, "_macos_startup.jou");
#endif

parse_file(&compst, command_line_args.infile, NULL);
parse_all_pending_files(&compst);
Expand Down
43 changes: 26 additions & 17 deletions src/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@
// readlink() stuff
#define _POSIX_C_SOURCE 200112L
#include <unistd.h>
#endif // _WIN32
#endif

#ifdef __APPLE__
#include <mach-o/dyld.h> // _NSGetExecutablePath
#endif

#include "util.h"
#include <ctype.h>
#include <errno.h>
#include <stdbool.h>
#include <stdnoreturn.h>
#include <stdio.h>
#include <stdlib.h>

Expand Down Expand Up @@ -58,28 +64,31 @@ void simplify_path(char *path)
// argv[0] doesn't work as expected when Jou is ran through PATH.
char *find_current_executable(void)
{
char *result;
const char *err;
char *result = NULL;
const char *err = NULL;

#ifdef _WIN32
extern char *_pgmptr; // A documented global variable in Windows. Full path to executable.
result = strdup(_pgmptr);
err = NULL;
#elif defined(__APPLE__)
uint32_t n = 1;
result = malloc(n);
int ret = _NSGetExecutablePath(result, &n); // sets n to desired size
assert(ret < 0); // didn't fit
result = realloc(result, n);
ret = _NSGetExecutablePath(result, &n);
assert(ret == 0);
#else
int n = 10000;
result = calloc(1, n);
ssize_t ret = readlink("/proc/self/exe", result, n);

if (ret < 0)
ssize_t ret;
int n = 1;
do {
n *= 2;
result = realloc(result, n);
memset(result, 0, n); // readlink() doesn't nul terminate
ret = readlink("/proc/self/exe", result, n);
} while (ret == n);
if (ret<0)
err = strerror(errno);
else if (ret == n) {
static char s[100];
sprintf(s, "path is more than %d bytes long", n);
err=s;
} else {
assert(0<ret && ret<n);
err = NULL;
}
#endif

if(err) {
Expand Down
26 changes: 26 additions & 0 deletions stdlib/_macos_startup.jou
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# A call to the _jou_macos_startup() function is inserted to the
# start of every Jou program when compiling for MacOS.

# On macos, the C "global variables" stdin, stdout and stderr have
# different names. For simplicity, Jou redefines them as variables
# with the same names and assigns the correct values to them.
#
# We can't import FILE from io.jou here, because then we would be
# trying to define a variable that already exists.
global stdin: void*
global stdout: void*
global stderr: void*
declare global __stdinp: void*
declare global __stdoutp: void*
declare global __stderrp: void*

def _jou_macos_startup() -> void:
stdin = __stdinp
stdout = __stdoutp
stderr = __stderrp

# On linux, C's errno is a macro that expands to (*__errno_location()).
# On macos it expands to (*__error()) instead. Let's make it consistent.
declare __error() -> int*
def __errno_location() -> int*:
return __error()
Loading

0 comments on commit 171226c

Please sign in to comment.