758 lines
28 KiB
Python
Executable file
758 lines
28 KiB
Python
Executable file
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright (c) 2014 Microsoft Corporation. All rights reserved.
|
|
# Released under Apache 2.0 license as described in the file LICENSE.
|
|
#
|
|
# Author: Soonho Kong
|
|
#
|
|
|
|
# This program contains code snippets from the Python six library
|
|
# released under the following LICENSE:
|
|
#
|
|
# Copyright (c) 2010-2015 Benjamin Peterson
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in all
|
|
# copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
# Python 2/3 compatibility
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import codecs
|
|
import fnmatch
|
|
import glob
|
|
import logging
|
|
import logging.handlers
|
|
import os
|
|
import platform
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
|
|
# Python 2/3 compatibility
|
|
if sys.version_info[0] == 3:
|
|
# Reset the output encoding globally (to utf-8)
|
|
if sys.stdout.encoding != 'utf-8':
|
|
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
|
|
if sys.stderr.encoding != 'utf-8':
|
|
sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict')
|
|
|
|
if sys.version_info[0] == 2:
|
|
def python_2_unicode_compatible(klass):
|
|
klass.__unicode__ = klass.__str__
|
|
klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
|
|
return klass
|
|
|
|
# Enforce subprocesses to use 'utf-8' in Python 2
|
|
reload(sys)
|
|
sys.setdefaultencoding("utf-8")
|
|
|
|
# Aliases
|
|
text_type = unicode
|
|
import itertools
|
|
filter = itertools.ifilter
|
|
map = itertools.imap
|
|
iteritems = dict.iteritems
|
|
|
|
from urllib import urlretrieve
|
|
elif sys.version_info[0] == 3:
|
|
def python_2_unicode_compatible(klass):
|
|
return klass
|
|
|
|
# Aliases
|
|
text_type = str
|
|
iteritems = dict.items
|
|
|
|
from urllib.request import urlretrieve
|
|
else:
|
|
sys.exit('Unsupported Python version')
|
|
|
|
# Fixate the path separator as '\' on Windows Platform
|
|
# even if users are on CYGWIN/MSYS2 environment
|
|
if platform.system() == "Windows":
|
|
os.path.sep = "\\"
|
|
|
|
# Reset MSYSTEM environment variable to enforce native-WINDOWS
|
|
# behavior to subprocesses.
|
|
if "MSYSTEM" in os.environ:
|
|
del os.environ["MSYSTEM"]
|
|
|
|
if platform.system().startswith("MSYS"):
|
|
# In MSYS platform, realpath has a strange behavior.
|
|
# os.path.realpath("c:\a\b\c") => \:\a\b\c
|
|
g_linja_path = os.path.abspath(os.path.normpath(__file__))
|
|
else:
|
|
g_linja_path = os.path.abspath(os.path.realpath(__file__))
|
|
g_lean_bin_dir = os.path.dirname(g_linja_path)
|
|
g_phony_targets = ["all", "clean", "tags", "clear-cache"]
|
|
g_project_filename = ".project"
|
|
g_lean_path = "USE DEFAULT" # System will search automatically
|
|
g_leantags_path = "USE DEFAULT" # System will search automatically
|
|
g_ninja_path = "USE DEFAULT" # System will search automatically
|
|
g_flycheck_header = "FLYCHECK_BEGIN"
|
|
g_flycheck_footer = "FLYCHECK_END"
|
|
g_lean_bin_dep_flag= "@LEAN_BIN_DEP@" == "ON"
|
|
|
|
g_logger = logging.getLogger('linja_logger')
|
|
g_debug_mode = False
|
|
|
|
@python_2_unicode_compatible
|
|
class FlycheckItem:
|
|
def __init__(self, filename, lineno, colno, ty, msg):
|
|
self.filename = filename
|
|
self.lineno = lineno
|
|
self.colno = colno
|
|
self.ty = ty
|
|
self.msg = msg
|
|
pass
|
|
def __str__(self):
|
|
ret = g_flycheck_header + " " + self.ty.upper() + "\n"
|
|
ret += "%s:%d:%d: %s: %s" % (self.filename, self.lineno, self.colno, self.ty, self.msg) + "\n"
|
|
ret += g_flycheck_footer
|
|
return ret
|
|
def __lt__(self, other):
|
|
return (self.filename, self.ty, self.lineno, self.colno) < (other.filename, other.ty, other.lineno, other.colno)
|
|
def loc(self):
|
|
return (self.filename, self.ty, self.lineno, self.colno)
|
|
@classmethod
|
|
def fromString(cls, target, text):
|
|
lines = text.strip().splitlines()
|
|
# Throw the first and last lines (header/footer)
|
|
lines = lines[1:-1]
|
|
try:
|
|
firstLine = lines[0]
|
|
items = [item.strip() for item in firstLine.split(":")]
|
|
filename = items[0]
|
|
lineno = int(items[1])
|
|
colno = int(items[2])
|
|
ty = items[3]
|
|
msg = ":".join(items[4:]) + "\n" + "\n".join(lines[1:])
|
|
msg = msg.strip()
|
|
return cls(filename, lineno, colno, ty, msg)
|
|
except:
|
|
return cls(target, 1, 0, "error", " ".join(lines))
|
|
|
|
@python_2_unicode_compatible
|
|
class FlycheckItemList:
|
|
def __init__(self, items):
|
|
self.items = items
|
|
def __str__(self):
|
|
return "\n".join([text_type(item) for item in self.items])
|
|
def __getitem__(self, i):
|
|
return self.items[i]
|
|
def __len__(self):
|
|
return len(self.items)
|
|
def sort(self):
|
|
self.items = sorted(self.items)
|
|
def filter(self, pred):
|
|
self.items = list(filter(pred, self.items))
|
|
def append(self, item):
|
|
self.items.append(item)
|
|
def truncate(self, n):
|
|
del self.items[n:]
|
|
def removeExtraItemsStartswith(self, text):
|
|
self.sort()
|
|
newItems = self.items[:1]
|
|
i = 1
|
|
while i < len(self.items):
|
|
prev_item = self.items[i-1]
|
|
cur_item = self.items[i]
|
|
if not cur_item.msg.startswith(text) or prev_item.loc() != cur_item.loc():
|
|
newItems.append(cur_item)
|
|
i += 1
|
|
self.items = newItems
|
|
|
|
@classmethod
|
|
def fromString(cls, target, text):
|
|
items = []
|
|
tmpBuffer = ""
|
|
ignore = True
|
|
for line in text.splitlines():
|
|
if line.startswith(g_flycheck_header):
|
|
tmpBuffer = tmpBuffer + line + "\n"
|
|
ignore = False
|
|
elif line.startswith(g_flycheck_footer):
|
|
tmpBuffer = tmpBuffer + line + "\n"
|
|
items.append(FlycheckItem.fromString(target, tmpBuffer.strip()))
|
|
tmpBuffer = ""
|
|
ignore = True
|
|
elif not ignore:
|
|
tmpBuffer = tmpBuffer + line + "\n"
|
|
return cls(items)
|
|
|
|
def init_logger():
|
|
formatter = logging.Formatter('[%(levelname)s] %(asctime)s %(message)s')
|
|
streamHandler = logging.StreamHandler()
|
|
streamHandler.setFormatter(formatter)
|
|
g_logger.addHandler(streamHandler)
|
|
if g_debug_mode == True:
|
|
fileHandler = logging.FileHandler('./linja.log')
|
|
fileHandler.setFormatter(formatter)
|
|
g_logger.addHandler(fileHandler)
|
|
|
|
def log(msg):
|
|
print(msg, file=sys.stderr)
|
|
|
|
def log_nonewline(msg):
|
|
print(("\r%s" % msg), end=' ', file=sys.stderr)
|
|
sys.stderr.flush()
|
|
|
|
def error(msg):
|
|
log("Error: %s" % msg)
|
|
exit(1)
|
|
|
|
class LinjaException(Exception):
|
|
"""Custom Exception"""
|
|
def __init__(self, msg):
|
|
self.msg = msg
|
|
def __str__(self):
|
|
return self.msg
|
|
|
|
def give_exec_permission(filename):
|
|
"""chmod +x filename"""
|
|
st = os.stat(filename)
|
|
os.chmod(filename, st.st_mode | stat.S_IEXEC)
|
|
|
|
def show_download_progress(*data):
|
|
file_size = int(data[2]/1000)
|
|
total_packets = data[2]/data[1]
|
|
downloaded_packets = data[0]
|
|
log_nonewline("Download: size\t= %i kb, packet: %i/%i" % (file_size, downloaded_packets, total_packets+1))
|
|
|
|
def get_ninja_url():
|
|
prefix = "https://leanprover.github.io/bin/"
|
|
if platform.architecture()[0] == "64bit":
|
|
if platform.system() == "Linux":
|
|
return prefix + "ninja-1.5.1-linux-x86_64"
|
|
elif platform.system() == "Windows":
|
|
return prefix + "ninja-1.5.1-win.exe"
|
|
elif platform.system() == "Darwin":
|
|
return prefix + "ninja-1.5.1-osx"
|
|
if platform.architecture()[0] == "32bit":
|
|
if platform.system() == "Linux":
|
|
return prefix + "ninja-1.5.1-linux-i386"
|
|
elif platform.system() == "Windows":
|
|
pass # TODO(soonhok): add support
|
|
elif platform.system() == "Darwin":
|
|
pass # TODO(soonhok): add support
|
|
error("we do not have ninja executable for this platform: %s" % platform.platform())
|
|
|
|
def build_ninja_and_save_at(ninja_path, platform):
|
|
saved_current_dir = os.getcwd()
|
|
tempdir = tempfile.mkdtemp()
|
|
build_dir = os.path.join(tempdir, "ninja")
|
|
built_ninja_path = os.path.join(build_dir, "ninja")
|
|
cmd_clone_ninja = ["git", "clone", "git://github.com/martine/ninja.git"]
|
|
cmd_checkout_release = ["git", "checkout", "release"]
|
|
cmd_bootstrap = [os.path.join(build_dir, "configure.py"), "--bootstrap", "--platform", platform]
|
|
try:
|
|
os.chdir(tempdir)
|
|
if subprocess.call(cmd_clone_ninja):
|
|
raise LinjaException("Failed to clone ninja repository")
|
|
os.chdir(build_dir)
|
|
if subprocess.call(cmd_checkout_release):
|
|
raise LinjaException("Failed to checkout release branch of ninja")
|
|
if subprocess.call(cmd_bootstrap):
|
|
raise LinjaException("Failed to build ninja")
|
|
shutil.copy2(built_ninja_path, ninja_path)
|
|
except IOError as e:
|
|
error(e)
|
|
except LinjaException as e:
|
|
error(e)
|
|
finally:
|
|
os.chdir(saved_current_dir)
|
|
shutil.rmtree(tempdir)
|
|
return ninja_path
|
|
|
|
def download_ninja_and_save_at(ninja_path):
|
|
if platform.system().startswith("CYGWIN"):
|
|
return build_ninja_and_save_at(ninja_path, "linux")
|
|
else:
|
|
url = get_ninja_url()
|
|
log("Downloading ninja: %s ===> %s\n" % (url, ninja_path))
|
|
tempdir = tempfile.mkdtemp()
|
|
temp_download_path = os.path.join(tempdir, os.path.split(ninja_path)[1])
|
|
urlretrieve(url, temp_download_path, show_download_progress)
|
|
log("\n")
|
|
if not os.path.isfile(temp_download_path):
|
|
error("failed to download ninja executable from %s" % url)
|
|
shutil.copy2(temp_download_path, ninja_path)
|
|
give_exec_permission(ninja_path)
|
|
return ninja_path
|
|
|
|
def find_file_upward(name, path = os.getcwd()):
|
|
project_file = os.path.join(path, name)
|
|
if os.path.isfile(project_file):
|
|
return path
|
|
up = os.path.dirname(path)
|
|
if up != path:
|
|
return find_file_upward(name, up)
|
|
return None
|
|
|
|
def escape_ninja_char(name):
|
|
return name.replace("$", "$$").replace(":", "$:").replace(" ", "$ ").replace("&", "$&")
|
|
|
|
def normalize_drive_name(name):
|
|
if platform.system() == "Windows":
|
|
drive, path = os.path.splitdrive(name)
|
|
if drive == None:
|
|
return name
|
|
else:
|
|
# Leo: return drive.lower() + path
|
|
return path
|
|
else:
|
|
return name
|
|
|
|
def process_target(target):
|
|
if target in g_phony_targets:
|
|
return target
|
|
return normalize_drive_name(os.path.abspath(target))
|
|
|
|
def which(program):
|
|
""" Lookup program in a path """
|
|
import os
|
|
def is_exe(fpath):
|
|
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
|
|
fpath, fname = os.path.split(program)
|
|
if fpath:
|
|
if is_exe(program):
|
|
return program
|
|
else:
|
|
for path in os.environ["PATH"].split(os.pathsep):
|
|
path = path.strip('"')
|
|
if "sbin" in path:
|
|
continue
|
|
exe_file = os.path.join(path, program)
|
|
if is_exe(exe_file):
|
|
return exe_file
|
|
return None
|
|
|
|
def find_location(exec_name, findLocal=True):
|
|
if findLocal:
|
|
pathname = os.path.join(os.path.dirname(g_linja_path), exec_name)
|
|
if os.path.isfile(pathname):
|
|
return pathname
|
|
pathname = which(exec_name) or os.path.join(g_lean_bin_dir, exec_name)
|
|
pathname = os.path.abspath(pathname)
|
|
return pathname
|
|
|
|
def check_requirements():
|
|
global g_lean_path, g_leantags_path, g_ninja_path
|
|
leantags_exec_name = "leantags"
|
|
lean_exec_name = "lean"
|
|
ninja_exec_name = "ninja"
|
|
if platform.system() == "Windows" or platform.system().startswith("MSYS"):
|
|
lean_exec_name = "lean.exe"
|
|
ninja_exec_name = "ninja.exe"
|
|
|
|
if g_lean_path == "USE DEFAULT":
|
|
g_lean_path = find_location(lean_exec_name, True)
|
|
if g_leantags_path == "USE DEFAULT":
|
|
g_leantags_path = find_location(leantags_exec_name, True)
|
|
if g_ninja_path == "USE DEFAULT":
|
|
g_ninja_path = find_location(ninja_exec_name, False)
|
|
if not os.path.isfile(g_lean_path):
|
|
error("cannot find lean executable at " + os.path.abspath(g_lean_path))
|
|
if not os.path.isfile(g_leantags_path):
|
|
error("cannot find leantags executable at " + os.path.abspath(g_leantags_path))
|
|
if not os.path.isfile(g_ninja_path):
|
|
g_ninja_path = download_ninja_and_save_at(g_ninja_path)
|
|
|
|
def process_args(args):
|
|
if (args.flycheck == False and args.flycheck_max_messages != None):
|
|
error("Please use --flycheck option with --flycheck-max-messages option.")
|
|
args.targets = list(map(process_target, args.targets))
|
|
if args.directory:
|
|
os.chdir(args.directory)
|
|
args.project_dir = find_file_upward(g_project_filename)
|
|
if args.project_dir:
|
|
os.chdir(args.project_dir)
|
|
if args.cache:
|
|
args.cache = process_target(args.cache)
|
|
if len(args.targets) != 1:
|
|
error("--cache option can only be used with one target")
|
|
if not args.cache.endswith(".lean") and not args.cache.endswith(".hlean"):
|
|
error("cache argument has to end with .lean or .hlean")
|
|
if args.cache.endswith(".lean"):
|
|
args.cache = args.cache[:-4] + "clean"
|
|
else:
|
|
args.cache = args.cache[:-5] + "clean"
|
|
args.phony_targets = list(set(g_phony_targets) & set(args.targets))
|
|
if args.verbose:
|
|
g_logger.setLevel(logging.INFO)
|
|
return args
|
|
|
|
def get_lean_options(args):
|
|
args.lean_options = []
|
|
if args.flycheck:
|
|
args.lean_options.append("--flycheck")
|
|
if args.discard:
|
|
args.lean_options.append("--discard")
|
|
if args.memory:
|
|
args.lean_options += ["-M", args.memory]
|
|
if args.trust:
|
|
args.lean_options += ["-t", args.trust]
|
|
if args.to_axiom:
|
|
args.lean_options.append("--to_axiom")
|
|
if args.cache:
|
|
args.lean_options += ["-c", '"%s"' % args.cache]
|
|
if args.lean_config_option:
|
|
for item in args.lean_config_option:
|
|
args.lean_options.append("-D" + item)
|
|
return args
|
|
|
|
def parse_arg(argv):
|
|
parser = argparse.ArgumentParser(description='linja: ninja build wrapper for Lean theorem prover.')
|
|
parser.add_argument('--flycheck', '-F', action='store_true', default=False, help="Use --flycheck option for Lean.")
|
|
parser.add_argument('--flycheck-max-messages', action='store', type=int, default=None, const=999999, nargs='?', help="Number of maximum flycheck messages to display.")
|
|
parser.add_argument('--cache', action='store', help="Use specified cache (clean) file.")
|
|
parser.add_argument('--directory', '-C', action='store', help="change to DIR before doing anything else.")
|
|
parser.add_argument('--lean-config-option', '-D', action='append', help="set a Lean configuration option (name=value)")
|
|
parser.add_argument('--verbose', '-v', action='store_true', help="turn on verbose option")
|
|
parser.add_argument('--memory', '-M', action='store', default=None, const=1, nargs='?', help="maximum amount of memory that can be used by Lean [default=unbounded]")
|
|
parser.add_argument('--keep-going', '-k', action='store', default=None, const=1, nargs='?', help="keep going until N jobs fail [default=1]")
|
|
parser.add_argument('--discard', '-r', action='store_true', default=False, help="discard the proof of imported theorems after checking")
|
|
parser.add_argument('--to_axiom', '-X', action='store_true', default=False, help="discard proofs of all theorems after checking them, i.e., theorems become axioms after checking")
|
|
parser.add_argument('--trust', '-t', action='store_true', default=False, help="trust level [default: max]")
|
|
parser.add_argument('targets', nargs='*')
|
|
args = parser.parse_args(argv)
|
|
check_requirements()
|
|
args = process_args(args)
|
|
args = get_lean_options(args)
|
|
return args
|
|
|
|
def debug_status(args):
|
|
print("Working Directory =", os.getcwd())
|
|
print("")
|
|
for key, val in iteritems(vars(args)):
|
|
print("Option[" + key + "] =", val)
|
|
print("")
|
|
print("linja path =", g_linja_path)
|
|
print("lean/bin dir =", g_lean_bin_dir)
|
|
print("phony targets =", g_phony_targets)
|
|
print("lean path =", g_lean_path)
|
|
print("leantags path =", g_leantags_path)
|
|
print("ninja path =", g_ninja_path)
|
|
|
|
def handle_flycheck_failure(out, err, args):
|
|
if len(args.targets) == 0:
|
|
error("handle_flycheck_failure is called without targets")
|
|
target = args.targets[0]
|
|
failed = set()
|
|
for line in out.splitlines():
|
|
if line.startswith("FAILED:"):
|
|
for lean_file in find_lean_files(args):
|
|
lean = lean_file['lean']
|
|
if lean in line and lean != target:
|
|
failed.add(lean)
|
|
for failed_file in failed:
|
|
print(g_flycheck_header, "ERROR")
|
|
print("%s:1:0: error: failed to compile %s" % (target, failed_file))
|
|
print(g_flycheck_footer)
|
|
if err:
|
|
print(g_flycheck_header, "ERROR")
|
|
print("%s:1:0: error: %s" % (target, err.strip()))
|
|
print(g_flycheck_footer)
|
|
if failed:
|
|
call_lean(target, args)
|
|
|
|
def process_lean_output(target, out, args, using_hlean):
|
|
n = args.flycheck_max_messages
|
|
if target.endswith(".olean"):
|
|
if using_hlean:
|
|
target = target[:-5] + "hlean"
|
|
else:
|
|
target = target[:-5] + "lean"
|
|
if (not target.endswith(".lean") and not using_hlean) or (not target.endswith(".hlean") and using_hlean):
|
|
print(out)
|
|
return
|
|
# Parse, filter, and remove extra items
|
|
flycheckItemList = FlycheckItemList.fromString(target, out)
|
|
flycheckItemList.filter(lambda item: item.filename == target)
|
|
flycheckItemList.removeExtraItemsStartswith("failed to add declaration")
|
|
# Only keep n items in the list.
|
|
# Add tooManyItemsError at the end if we truncated the list
|
|
if n and len(flycheckItemList) > n:
|
|
count = len(flycheckItemList)
|
|
flycheckItemList.truncate(n)
|
|
tooManyItemsError = FlycheckItem(target, 1, 0, "error", "For performance, we only display %d errors/warnings out of %d. (lean-flycheck-max-messages-to-display)" % (n, count))
|
|
flycheckItemList.append(tooManyItemsError)
|
|
print(flycheckItemList)
|
|
|
|
def call_ninja(args):
|
|
targets = []
|
|
for item in args.targets:
|
|
if item.endswith(".lean"):
|
|
targets.append(item[:-4] + "olean")
|
|
elif item.endswith(".hlean"):
|
|
targets.append(item[:-5] + "olean")
|
|
else:
|
|
targets.append(item)
|
|
proc_out = proc_err = None
|
|
if args.flycheck:
|
|
proc_out = subprocess.PIPE
|
|
proc_err = subprocess.PIPE
|
|
ninja_option = []
|
|
if args.keep_going:
|
|
ninja_option += ["-k", args.keep_going]
|
|
proc = subprocess.Popen([g_ninja_path] + ninja_option + targets, stdout=proc_out, stderr=proc_err)
|
|
(out, err) = proc.communicate()
|
|
if out is not None:
|
|
out = out.decode('utf-8').replace('\r\n', '\n')
|
|
if err is not None:
|
|
err = err.decode('utf-8').replace('\r\n', '\n')
|
|
if args.flycheck:
|
|
if len(args.targets) == 1 and (args.targets[0].endswith(".lean") or args.targets[0].endswith(".hlean")):
|
|
process_lean_output(targets[0], out, args, args.targets[0].endswith(".hlean"))
|
|
handle_flycheck_failure(out, err, args)
|
|
else:
|
|
print(out + err)
|
|
|
|
return proc.returncode
|
|
|
|
def call_lean(filename, args):
|
|
proc = subprocess.Popen([g_lean_path] + args.lean_options + [filename],
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
out = proc.communicate()[0].decode('utf-8').replace('\r\n', '\n')
|
|
process_lean_output(filename, out, args, filename.endswith(".hlean"))
|
|
return proc.returncode
|
|
|
|
def get_lean_names(lean_file, args, using_hlean):
|
|
lean_file = os.path.abspath(lean_file)
|
|
basename, ext = os.path.splitext(lean_file)
|
|
basename = normalize_drive_name(basename)
|
|
item = {"base" : basename}
|
|
lean_name_exts = ["lean", "olean", "clean", "ilean", "d"]
|
|
for ext in lean_name_exts:
|
|
if ext == "lean" and using_hlean:
|
|
item[ext] = basename + ".hlean"
|
|
else:
|
|
item[ext] = basename + "." + ext
|
|
if args.cache and len(args.targets) == 1 and item['lean'] == args.targets[0]:
|
|
item['clean'] = args.cache
|
|
return item
|
|
|
|
def recursive_glob(directory):
|
|
return [os.path.join(dirpath, f)
|
|
for dirpath, dirnames, files in os.walk(directory)
|
|
for f in fnmatch.filter(files, '*')]
|
|
|
|
def find_files(directory, pattern):
|
|
if pattern.endswith("/"):
|
|
return recursive_glob(os.path.join(directory, pattern))
|
|
matches = []
|
|
for root, dirnames, filenames in os.walk(directory):
|
|
for filename in fnmatch.filter(filenames, pattern):
|
|
matches.append(normalize_drive_name(os.path.join(root, filename)))
|
|
return matches
|
|
|
|
def find_lean_files(args):
|
|
"""Find lean files under project directory. Include and exclude
|
|
files based on patterns in .project file. Use static cache to
|
|
compute only once and reuse it"""
|
|
project_dir = args.project_dir
|
|
if find_lean_files.cached_list != []:
|
|
return find_lean_files.cached_list
|
|
files = set()
|
|
include_patterns, exclude_patterns = [], []
|
|
with open(os.path.join(project_dir, g_project_filename), 'r') as f:
|
|
for line in f:
|
|
c = line[0]
|
|
if c == '+':
|
|
include_patterns.append(line[1:].strip())
|
|
elif c == '-':
|
|
exclude_patterns.append(line[1:].strip())
|
|
elif c == '#':
|
|
pass # Comment
|
|
elif c == 'T':
|
|
pass # TAG
|
|
elif c == 'O':
|
|
pass # Lean Option
|
|
for pattern in include_patterns:
|
|
files |= set(find_files(project_dir, pattern))
|
|
for pattern in exclude_patterns:
|
|
files -= set(find_files(project_dir, pattern))
|
|
has_lean = False
|
|
has_hlean = False
|
|
for file in files:
|
|
if file.endswith(".lean"):
|
|
has_lean = True
|
|
if file.endswith(".hlean"):
|
|
has_hlean = True
|
|
if has_lean and has_hlean:
|
|
error("project cannot mix .lean and .hlean files")
|
|
for file in args.targets:
|
|
if file.endswith(".lean") or file.endswith(".hlean"):
|
|
files.add(file)
|
|
elif file.endswith(".olean"):
|
|
if has_hlean:
|
|
file.add(file[:-5] + "hlean")
|
|
else:
|
|
file.add(file[:-5] + "lean")
|
|
for f in files:
|
|
find_lean_files.cached_list.append(get_lean_names(f, args, has_hlean))
|
|
return find_lean_files.cached_list
|
|
|
|
# Initialize static variable
|
|
find_lean_files.cached_list = []
|
|
|
|
def clear_cache(args):
|
|
files = find_lean_files(args)
|
|
files = find_lean_files(args)
|
|
files = find_lean_files(args)
|
|
files = find_lean_files(args)
|
|
num_of_files = len(files)
|
|
i = 0
|
|
for item in files:
|
|
i += 1
|
|
clean_file = item['clean']
|
|
if os.path.isfile(clean_file):
|
|
sys.stderr.write("[%i/%i] clear cache... % -80s\r" % (i, num_of_files, clean_file))
|
|
sys.stderr.flush()
|
|
os.remove(clean_file)
|
|
if num_of_files > 0:
|
|
sys.stderr.write("\n")
|
|
|
|
def build_olean(lean, olean, clean, dlean, ilean, base):
|
|
(lean, olean, clean, dlean, ilean, base) = list(map(escape_ninja_char, (lean, olean, clean, dlean, ilean, base)))
|
|
if clean.startswith(base):
|
|
str = """build %s %s %s: LEAN %s | %s""" % (olean, ilean, clean, lean, dlean)
|
|
else:
|
|
str = """build %s %s: LEAN %s | %s""" % (olean, ilean, lean, dlean)
|
|
if g_lean_bin_dep_flag:
|
|
str += " %s" % escape_ninja_char(normalize_drive_name(g_lean_path))
|
|
str += "\n"
|
|
str += " DLEAN_FILE=%s\n" % dlean
|
|
str += " OLEAN_FILE=%s\n" % olean
|
|
str += " CLEAN_FILE=%s\n" % clean
|
|
str += " ILEAN_FILE=%s\n" % ilean
|
|
return str
|
|
|
|
def make_build_ninja(args):
|
|
with open(os.path.join(args.project_dir, "build.ninja"), "w") as f:
|
|
lean_files = find_lean_files(args)
|
|
|
|
print("""rule CLEAN""", file=f)
|
|
print(""" command = """, end=' ', file=f)
|
|
print(""""%s" -t clean""" % g_ninja_path, file=f)
|
|
print(""" description = Cleaning all built files...""", file=f)
|
|
|
|
print("""rule LEAN""", file=f)
|
|
print(""" depfile = ${DLEAN_FILE}""", file=f)
|
|
print(""" command = "%s" %s $in -o "${OLEAN_FILE}" -c "${CLEAN_FILE}" -i "${ILEAN_FILE}" """ \
|
|
% (g_lean_path, " ".join(args.lean_options)), file=f)
|
|
|
|
print("""rule LEANTAGS""", file=f)
|
|
print(""" command = """, end=' ', file=f)
|
|
if platform.system() == "Windows":
|
|
print("python ", end=' ', file=f)
|
|
print(""""%s" --relative -- $in """ % (g_leantags_path), file=f)
|
|
|
|
print("build all: phony", end=' ', file=f)
|
|
for item in lean_files:
|
|
print(" " + escape_ninja_char(item['olean']), end=' ', file=f)
|
|
print("", file=f)
|
|
|
|
tags_file = "TAGS"
|
|
print("build tags: phony " + tags_file, file=f)
|
|
print("build " + tags_file + ": LEANTAGS", end=' ', file=f)
|
|
for item in lean_files:
|
|
print(" " + escape_ninja_char(item['ilean']), end=' ', file=f)
|
|
print("", file=f)
|
|
|
|
print("""build clean: CLEAN""", file=f)
|
|
|
|
for item in lean_files:
|
|
print(build_olean(item['lean'], item['olean'], item['clean'], item['d'], item['ilean'], item['base']), file=f)
|
|
|
|
print("""default all""", file=f)
|
|
|
|
def escape_dep(s):
|
|
return s.replace(" ", "\\ ")
|
|
|
|
def make_deps(lean_file, dlean_file, olean_file):
|
|
with open(dlean_file, "w") as f:
|
|
deps = []
|
|
proc = subprocess.Popen([g_lean_path, "--deps", lean_file],
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
output = proc.communicate()[0].decode('utf-8').replace('\r\n', '\n')
|
|
print(escape_dep(olean_file) + ": \\", file=f)
|
|
for olean_file in output.strip().splitlines():
|
|
if olean_file:
|
|
deps.append(normalize_drive_name(os.path.abspath(olean_file)))
|
|
deps = list(map(escape_dep, deps))
|
|
deps_str = " " + (" \\\n ".join(deps))
|
|
print(deps_str, file=f)
|
|
|
|
def make_deps_all_files(args):
|
|
lean_files = find_lean_files(args)
|
|
threads = []
|
|
i, num_of_files = 1, len(lean_files)
|
|
for item in lean_files:
|
|
lean_file = item['lean']
|
|
dlean_file = item['d']
|
|
olean_file = item['olean']
|
|
if not os.path.isfile(dlean_file) or os.path.getmtime(dlean_file) < os.path.getmtime(lean_file):
|
|
thread = threading.Thread(target=make_deps, args = [lean_file, dlean_file, olean_file])
|
|
sys.stderr.write("[%i/%i] generating dep... % -80s" % (i, num_of_files, dlean_file))
|
|
if args.flycheck == True:
|
|
sys.stderr.write("\n")
|
|
else:
|
|
sys.stderr.write("\r")
|
|
sys.stderr.flush()
|
|
thread.start()
|
|
threads.append(thread)
|
|
i += 1
|
|
for thread in threads:
|
|
thread.join()
|
|
if threads != []:
|
|
sys.stderr.write("\n")
|
|
|
|
def main(argv=None):
|
|
init_logger()
|
|
if argv is None:
|
|
argv = sys.argv[1:]
|
|
args = parse_arg(argv)
|
|
if args.project_dir:
|
|
if args.targets == ["clear-cache"]:
|
|
args.targets = []
|
|
clear_cache(args)
|
|
return 0
|
|
if not "clean" in args.targets:
|
|
make_deps_all_files(args)
|
|
make_build_ninja(args)
|
|
return call_ninja(args)
|
|
|
|
else: # NO Project Directory Found
|
|
if args.phony_targets:
|
|
error("cannot find project directory. Make sure that you have " \
|
|
+ g_project_filename + " file at the project root.")
|
|
returncode = 0
|
|
for filename in args.targets:
|
|
if os.path.isfile(filename) and (filename.endswith(".lean") or filename.endswith(".hlean")):
|
|
returncode |= call_lean(filename, args)
|
|
return returncode
|
|
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|