2021-01-05 13:02:14 +01:00
#!/usr/bin/env python3
2020-12-26 20:05:45 +01:00
2020-11-02 20:06:52 +01:00
# Copyright 2020 Mattia Giambirtone
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# A minimalistic build script for JAPL
import os
2021-01-14 16:03:31 +01:00
import stat
2020-11-02 20:06:52 +01:00
import shlex
2021-01-14 16:03:31 +01:00
import shutil
2020-11-02 20:06:52 +01:00
import logging
import argparse
from time import time
from typing import Dict
from pprint import pformat
2021-01-09 18:03:47 +01:00
from subprocess import Popen , PIPE , DEVNULL , run
2020-11-02 20:06:52 +01:00
2020-11-02 21:46:33 +01:00
2020-11-02 20:06:52 +01:00
CONFIG_TEMPLATE = ''' # Copyright 2020 Mattia Giambirtone
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import strformat
const MAP_LOAD_FACTOR * = { map_load_factor } # Load factor for builtin hashmaps (TODO)
const ARRAY_GROW_FACTOR * = { array_grow_factor } # How much extra memory to allocate for dynamic arrays (TODO)
2021-01-10 09:49:35 +01:00
const FRAMES_MAX * = { frames_max } # The maximum recursion limit
2020-11-02 20:06:52 +01:00
const JAPL_VERSION * = " 0.3.0 "
const JAPL_RELEASE * = " alpha "
const DEBUG_TRACE_VM * = { debug_vm } # Traces VM execution
const DEBUG_TRACE_GC * = { debug_gc } # Traces the garbage collector (TODO)
const DEBUG_TRACE_ALLOCATION * = { debug_alloc } # Traces memory allocation/deallocation (WIP)
const DEBUG_TRACE_COMPILER * = { debug_compiler } # Traces the compiler
const JAPL_VERSION_STRING * = & " JAPL {{ JAPL_VERSION}} ( {{ JAPL_RELEASE}}, {{ CompileDate}} {{ CompileTime}}) "
const HELP_MESSAGE * = """ The JAPL runtime interface, Copyright (C) 2020 Mattia Giambirtone
This program is free software , see the license distributed with this program or check
http : / / www . apache . org / licenses / LICENSE - 2.0 for more info .
Basic usage
- - - - - - - - - - -
$ jpl - > Start the REPL
$ jpl filename . jpl - > Run filename . jpl
Command - line options
- - - - - - - - - - - - - - - - - - - -
- h , - - help - > Show this help text and exit
- v , - - version - > Print the JAPL version number and exit
2021-01-10 09:49:35 +01:00
- c - > Executes the passed string
2020-11-02 20:06:52 +01:00
""" ' ' '
2021-01-14 16:03:31 +01:00
def run_command ( command : str , mode : str = " Popen " , * * kwargs ) :
"""
Runs a command with subprocess and returns the process '
return code , stderr and stdout
"""
if mode == " Popen " :
process = Popen ( shlex . split ( command , posix = os . name != " nt " ) , * * kwargs )
else :
process = run ( command , * * kwargs )
if mode == " Popen " :
stdout , stderr = process . communicate ( )
else :
stdout , stderr = None , None
return stdout , stderr , process . returncode
def build ( path : str , flags : Dict [ str , str ] = { } , options : Dict [ str , bool ] = { } , override : bool = False , skip_tests : bool = False , install : bool = False ,
ignore_binary : bool = False ) :
2020-11-02 20:06:52 +01:00
"""
Compiles the JAPL runtime , generating the appropriate
2021-01-14 16:03:31 +01:00
configuration needed for compilation to succeed ,
running tests and performing installation
when possible .
2020-11-02 20:06:52 +01:00
Nim 1.2 or above is required to build JAPL
2020-11-02 21:37:57 +01:00
: param path : The path to JAPL ' s main source directory
2020-11-02 20:06:52 +01:00
: type path : string , optional
: param flags : Any extra compiler flags that will
be passed to nim . Keys and values should be strings ,
but any object with a proper __str__ or __repr__
method is probably fine as long as it ' s a valid
nim option for the compiler . Defaults to { } ( no flags )
: type flags : dict , optional
: param options : Compile - time options such as debugging the
compiler or the VM , defaults to { } ( use defaults )
: type options : dict , optional
"""
2020-11-02 21:37:57 +01:00
config_path = os . path . join ( path , " config.nim " )
main_path = os . path . join ( path , " japl.nim " )
2021-01-14 16:03:31 +01:00
logging . info ( " Just Another Build Tool, version 0.3.2 " )
2020-11-02 20:06:52 +01:00
if not os . path . exists ( path ) :
logging . error ( f " Input path ' { path } ' does not exist " )
return
2020-11-02 21:37:57 +01:00
if os . path . isfile ( config_path ) and not override :
logging . warning ( f " A config file exists at ' { config_path } ' , keeping it " )
2020-11-02 20:06:52 +01:00
else :
2020-12-26 17:01:03 +01:00
logging . warning ( f " Overriding config file at ' { config_path } ' " )
2020-11-02 21:37:57 +01:00
logging . debug ( f " Generating config file at ' { config_path } ' " )
try :
with open ( config_path , " w " ) as build_config :
2021-01-09 18:03:47 +01:00
build_config . write ( CONFIG_TEMPLATE . format ( * * options ) )
2020-11-02 21:37:57 +01:00
except Exception as fatal :
logging . error ( f " A fatal unhandled exception occurred -> { type ( fatal ) . __name__ } : { fatal } " )
2021-01-09 18:03:47 +01:00
return
2020-11-02 21:37:57 +01:00
else :
logging . debug ( f " Config file has been generated, compiling with options as follows: \n { pformat ( options , indent = 2 ) } " )
logging . debug ( f " Compiling ' { main_path } ' " )
2020-11-02 20:06:52 +01:00
nim_flags = " " . join ( f " - { name } : { value } " if len ( name ) == 1 else f " -- { name } : { value } " for name , value in flags . items ( ) )
2021-01-09 18:03:47 +01:00
command = " nim {flags} compile {path} " . format ( flags = nim_flags , path = main_path )
2020-11-02 20:06:52 +01:00
logging . debug ( f " Running ' { command } ' " )
logging . info ( " Compiling JAPL " )
start = time ( )
2021-01-14 16:03:31 +01:00
_ , stderr , status = run_command ( command , stdout = DEVNULL , stderr = PIPE )
if status != 0 :
logging . error ( f " Command ' { command } ' exited with non-0 exit code { status } , output below: \n { stderr . decode ( ) } " )
2020-11-02 20:06:52 +01:00
else :
logging . debug ( f " Compilation completed in { time ( ) - start : .2f } seconds " )
logging . info ( " Build completed " )
2021-01-09 18:03:47 +01:00
if skip_tests :
logging . warning ( " Skipping test suite " )
else :
logging . info ( " Running tests under tests/ " )
logging . debug ( " Compiling test suite " )
start = time ( )
tests_path = " ./tests/runtests " if os . name != " nt " else " . \t ests \r untests "
2021-01-14 16:03:31 +01:00
_ , stderr , status = run_command ( f " nim compile { tests_path } " , stdout = DEVNULL , stderr = PIPE )
if status != 0 :
logging . error ( f " Command ' { command } ' exited with non-0 exit code { status } , output below: \n { stderr . decode ( ) } " )
2021-01-09 18:03:47 +01:00
else :
logging . debug ( f " Test suite compilation completed in { time ( ) - start : .2f } seconds " )
logging . debug ( " Running tests " )
start = time ( )
2021-01-14 16:03:31 +01:00
# TODO: Find a better way of running the test suite
process = run_command ( f " { tests_path } " , mode = " run " , shell = True , stderr = PIPE )
if status != 0 :
logging . error ( f " Command ' { command } ' exited with non-0 exit code { status } , output below: \n { stderr . decode ( ) } " )
2021-01-09 18:03:47 +01:00
else :
logging . debug ( f " Test suite ran in { time ( ) - start : .2f } seconds " )
logging . info ( " Test suite completed! " )
2021-01-14 16:03:31 +01:00
if args . install :
if os . name == " nt " :
logging . warning ( " Sorry, but automatically installing JAPL is not yet supported on windows " )
else :
# TODO -> Is PATH defined on all linux distros?
logging . info ( f " Installing JAPL at PATH " )
if any ( os . path . exists ( os . path . join ( path , " jpl " ) ) for path in os . getenv ( " PATH " ) . split ( " : " ) ) and not ignore_binary :
logging . error ( " Could not install JAPL because a binary already exists in PATH " )
return
install_path = os . path . join ( os . getenv ( " PATH " ) . split ( " : " ) [ 0 ] , " jpl " )
for path in os . getenv ( " PATH " ) . split ( " : " ) :
install_path = os . path . join ( path , " jpl " )
logging . debug ( f " Attempting to install JAPL at ' { install_path } ' " )
try :
shutil . move ( main_path . strip ( " .nim " ) , install_path )
except PermissionError :
logging . debug ( f " Path ' { path } ' is not writable, attempting next entry in PATH " )
except Exception as fatal :
logging . error ( f " A fatal unhandled exception occurred -> { type ( fatal ) . __name__ } : { fatal } " )
else :
logging . debug ( f " JAPL installed at ' { path } ' , setting executable permissions " )
# TODO: Use external oschmod library once we support windows!
try :
perms = os . stat ( install_path )
os . chmod ( install_path , perms . st_mode | stat . S_IXUSR | stat . S_IXGRP | stat . S_IXOTH )
except Exception as fatal :
logging . error ( f " A fatal unhandled exception occurred -> { type ( fatal ) . __name__ } : { fatal } " )
break
2020-11-02 20:06:52 +01:00
if __name__ == " __main__ " :
parser = argparse . ArgumentParser ( )
2020-11-02 21:37:57 +01:00
parser . add_argument ( " path " , help = " The path to JAPL ' s source directory " )
2021-01-14 16:03:31 +01:00
parser . add_argument ( " --verbose " , help = " Prints debug information to stdout " , action = " store_true " , default = os . environ . get ( " JAPL_VERBOSE " ) )
parser . add_argument ( " --flags " , help = " Optional flags to be passed to the nim compiler. Must be a comma-separated list of name:value (without spaces) " , default = os . environ . get ( " JAPL_FLAGS " ) )
parser . add_argument ( " --options " , help = " Set compile-time options and constants, pass a comma-separated list of name:value (without spaces). "
" Note that if a config.nim file exists in the destination directory, that will override any setting defined here unless --override-config is used " , default = os . environ . get ( " JAPL_OPTIONS " ) )
parser . add_argument ( " --override-config " , help = " Overrides the setting of an already existing config.nim file in the destination directory " , action = " store_true " , default = os . environ . get ( " JAPL_OVERRIDE_CONFIG " ) )
parser . add_argument ( " --skip-tests " , help = " Skips running the JAPL test suite, useful for debug builds " , action = " store_true " , default = os . environ . get ( " JAPL_SKIP_TESTS " ) )
parser . add_argument ( " --keep-results " , help = " Instructs the build tool not to delete the testresults.txt file from the test suite, useful for debugging " , action = " store_true " , default = os . environ . get ( " JAPL_KEEP_RESULTS " ) )
parser . add_argument ( " --install " , help = " Tries to move the compiled binary to PATH (this is always disabled on windows) " , action = " store_true " , default = os . environ . get ( " JAPL_INSTALL " ) )
parser . add_argument ( " --ignore-binary " , help = " Ignores an already existing ' jpl ' binary in any installation directory and overwrites it, use (with care!) with --install " , action = " store_true " , default = os . environ . get ( " JAPL_IGNORE_BINARY " ) )
2020-11-02 20:06:52 +01:00
args = parser . parse_args ( )
2020-12-28 10:09:52 +01:00
flags = {
2021-01-14 16:03:31 +01:00
" gc " : " markAndSweep " , # Because refc is broken ¯\_(ツ)_/¯
2020-12-28 10:09:52 +01:00
}
2020-11-02 20:06:52 +01:00
options = {
" debug_vm " : " false " ,
" debug_gc " : " false " ,
" debug_compiler " : " false " ,
" debug_alloc " : " false " ,
" map_load_factor " : " 0.75 " ,
" array_grow_factor " : " 2 " ,
2021-01-09 18:03:47 +01:00
" frames_max " : " 800 " ,
2020-11-02 20:06:52 +01:00
}
2021-01-14 16:03:31 +01:00
# We support environment variables!
for key , value in options . items ( ) :
if var := os . getenv ( f " JAPL_ { key . upper ( ) } " ) :
options [ key ] = var
2020-11-02 20:06:52 +01:00
logging . basicConfig ( format = " [ %(levelname)s - %(asctime)s ] %(message)s " ,
datefmt = " % T " ,
2021-01-14 16:03:31 +01:00
level = logging . DEBUG if args . verbose else logging . INFO
2020-11-02 20:06:52 +01:00
)
if args . flags :
try :
for value in args . flags . split ( " , " ) :
k , v = value . split ( " : " , maxsplit = 2 )
flags [ k ] = v
except Exception :
logging . error ( " Invalid parameter for --flags " )
exit ( )
if args . options :
try :
for value in args . options . split ( " , " ) :
k , v = value . split ( " : " , maxsplit = 2 )
if k not in options :
2021-01-14 16:03:31 +01:00
logging . error ( " Invalid compile-time option ' {key} ' " )
2020-11-02 20:06:52 +01:00
exit ( )
options [ k ] = v
except Exception :
logging . error ( " Invalid parameter for --options " )
exit ( )
2021-01-14 16:03:31 +01:00
build ( args . path , flags , options , args . override_config , args . skip_tests , args . install , args . ignore_binary )
if not args . keep_results and not args . skip_tests :
if os . path . isfile ( " testresults.txt " ) :
try :
os . remove ( " testresults.txt " )
except Exception as error :
logging . warning ( f " Could not remove test results file due to a { type ( error ) . __name__ } : { error } " )
2020-11-02 20:06:52 +01:00
logging . debug ( " Build tool exited " )