208 lines
7.0 KiB
Python
208 lines
7.0 KiB
Python
import datetime
|
|
from pathlib import Path
|
|
import sys
|
|
from typing import Annotated, Optional
|
|
import typer
|
|
from subprocess import Popen, PIPE
|
|
import subprocess
|
|
import os
|
|
import re
|
|
import shutil
|
|
sys.stdin.reconfigure(encoding='utf-8')
|
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
|
|
__VERSION__ = "1.2"
|
|
__AUTHORS__ = ("Dj_Haski",)
|
|
__LICENSE__ = "MIT"
|
|
app = typer.Typer()
|
|
|
|
|
|
def print_header():
|
|
print(f'Fortrun by {", ".join(__AUTHORS__)} v{__VERSION__} ({__LICENSE__})')
|
|
print(
|
|
f'Copyright (c) 2022-{datetime.datetime.now().year}, {", ".join(__AUTHORS__)}\n'
|
|
)
|
|
|
|
|
|
def errexit(message: str):
|
|
typer.echo(typer.style(f"error: ", fg=typer.colors.RED) + message)
|
|
typer.echo(
|
|
"Compilation " + typer.style(f"failed", fg=typer.colors.RED) + ", exiting..."
|
|
)
|
|
sys.exit(-1)
|
|
|
|
|
|
def info(message: str):
|
|
typer.echo(typer.style(f"info: ", fg=typer.colors.CYAN) + message)
|
|
|
|
|
|
def hint(message: str):
|
|
typer.echo(typer.style(f"hint: ", fg=typer.colors.YELLOW) + message)
|
|
|
|
def safe_check_output(command, encoding='utf-8'):
|
|
try:
|
|
return subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, encoding=encoding)
|
|
except subprocess.CalledProcessError as e:
|
|
return e.output
|
|
|
|
def try_or_exit(command: str):
|
|
try:
|
|
subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT)
|
|
return True
|
|
except subprocess.CalledProcessError as e:
|
|
hint(
|
|
"Looks like compilation or linking failed. Perhaps you forgot to add some libraries or some source files? Check your source code and try again."
|
|
)
|
|
errexit(f"Failed to execute command '{command}'")
|
|
|
|
|
|
def format_number(number: float):
|
|
if number.is_integer():
|
|
return str(int(number)).ljust(12)
|
|
else:
|
|
return f"{number:.12f}".rstrip('0').ljust(12)
|
|
|
|
def check_fl32_output():
|
|
output = safe_check_output('fl32.exe')
|
|
if "Microsoft (R) Fortran PowerStation" in output:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def get_line(file: Path, line: int):
|
|
with open(file, 'r', encoding='utf-8') as f:
|
|
return f.readlines()[line-1]
|
|
|
|
@app.command()
|
|
def build(
|
|
input_files: list[Path] = typer.Argument(help="List of source files and libraries"),
|
|
output: Path = typer.Option(
|
|
help="Output executable", default=Path("app.exe")),
|
|
run: Annotated[
|
|
bool, typer.Option(help="Run the executable after compilation")
|
|
] = False,
|
|
friendly: Annotated[bool, typer.Option(help="Convert fortran numbers to friendly format")] = False,
|
|
clean: bool = typer.Option(help="Clean build folder after compilation", default=True),
|
|
pause: bool = typer.Option(help="Pause after compilation", default=False),
|
|
):
|
|
"""
|
|
Build executable from source files and libraries.
|
|
"""
|
|
if not check_fl32_output():
|
|
errexit('Microsoft Fortran PowerStation fl32.exe compiler is not installed. Please install it and try again.')
|
|
|
|
tmp_folder = Path("build/")
|
|
if tmp_folder.exists():
|
|
shutil.rmtree(tmp_folder)
|
|
os.mkdir(tmp_folder)
|
|
|
|
if not input_files:
|
|
errexit("No input files were specified.")
|
|
|
|
for file in input_files:
|
|
if not file.exists():
|
|
errexit(f'File "{file}" was not found in current directory.')
|
|
|
|
f90_files = [file for file in input_files if file.suffix.lower() == ".f90"]
|
|
lib_files = [str(file) for file in input_files if file.suffix.lower() == ".lib"]
|
|
|
|
info(f'Source files to be compiled: {", ".join(map(str, f90_files))}')
|
|
if lib_files:
|
|
info(f'Libraries to be linked: {", ".join(lib_files)}')
|
|
info(f'Output executable: "{output}"')
|
|
|
|
info("Compiling files...")
|
|
stats = {}
|
|
for f90_file in f90_files:
|
|
compile_output = safe_check_output(f'fl32 /c /Fo"build\\{f90_file.stem}" /nologo {f90_file}')
|
|
failed = False
|
|
for line in compile_output.split('\n'):
|
|
match = re.fullmatch('(.+)\(([0-9]+)\): (\S+) (\S+): (.+)', line)
|
|
if match:
|
|
file, line, info_type, code, message = match.groups()
|
|
|
|
if info_type == 'error':
|
|
typer.echo(typer.style(f"\nerror: ", fg=typer.colors.RED) + f'{message}, file {file} at line {line} [{code}]')
|
|
fl = get_line(Path(file), int(line)).strip()
|
|
typer.echo(f'{line} {fl}')
|
|
typer.echo(typer.style(f'{" " * len(line)} {"^" * len(fl)}\n', fg=typer.colors.RED))
|
|
failed = True
|
|
stats['errors'] = stats.get('errors', 0) + 1
|
|
if info_type == 'warning':
|
|
typer.echo(typer.style(f"\nwarning: ", fg=typer.colors.YELLOW) + f'{message}, file {file} at line {line} [{code}]')
|
|
fl = get_line(Path(file), int(line)).strip()
|
|
typer.echo(f'{line} {fl}')
|
|
typer.echo(typer.style(f'{" " * len(line)} {"^" * len(fl)}\n', fg=typer.colors.YELLOW))
|
|
stats['warnings'] = stats.get('warnings', 0) + 1
|
|
|
|
if failed:
|
|
errexit(f'Failed to compile "{f90_file}", errors listed above. Check your source code and try again.')
|
|
|
|
info("Linking to executable...")
|
|
joined_files_with_dist = [
|
|
str(Path("build") / (file.stem + ".obj")) for file in f90_files
|
|
]
|
|
try_or_exit(
|
|
f'fl32 /nologo {" ".join(joined_files_with_dist)} {" ".join(lib_files)} /Fe"{output}"'
|
|
)
|
|
|
|
if clean:
|
|
shutil.rmtree(tmp_folder)
|
|
|
|
|
|
info(f"Compilation finished to '{output}'. Errors: {stats.get('errors', 0)}, Warnings: {stats.get('warnings', 0)}")
|
|
|
|
if run:
|
|
info(f'Executing "{output}"...')
|
|
hint("If nothing happens, press ENTER to continue...")
|
|
if not friendly:
|
|
process = Popen([output], stdout=PIPE)
|
|
(output, _) = process.communicate()
|
|
_ = process.wait()
|
|
|
|
output = output.decode("cp866")
|
|
print(output)
|
|
|
|
if pause:
|
|
input('Press ENTER to continue... ')
|
|
|
|
sys.exit(0)
|
|
info('Running in friendly mode! :)')
|
|
process = Popen([output], stdout=PIPE)
|
|
(output, _) = process.communicate()
|
|
_ = process.wait()
|
|
|
|
output = output.decode("cp866")
|
|
|
|
for match in re.findall("(\.(\d+)D\+(\d+))", output):
|
|
number = float(f"0.{match[1]}")
|
|
for _ in range(int(match[2].strip("0") or "0")):
|
|
number *= 10
|
|
|
|
output = output.replace(match[0], format_number(number))
|
|
|
|
for match in re.findall("(\.(\d+)D\-(\d+))", output):
|
|
number = float(f"0.{match[1]}")
|
|
for _ in range(int(match[2].strip("0") or "0")):
|
|
number /= 10
|
|
|
|
output = output.replace(match[0], format_number(number))
|
|
|
|
print(output)
|
|
|
|
if pause:
|
|
input('Press ENTER to continue... ')
|
|
|
|
|
|
@app.command()
|
|
def version():
|
|
"""
|
|
Prints the version of the tool.
|
|
"""
|
|
print(
|
|
"Developed for Moscow Aviation University (MAU) for building legacy Fortran projects with ease. Special for 609 students with love."
|
|
)
|
|
|
|
print_header()
|
|
app()
|