Skip to content

Commit

Permalink
Initial Checkin
Browse files Browse the repository at this point in the history
  • Loading branch information
Beakerboy committed Feb 21, 2024
1 parent 6cdcf1b commit 4fc4888
Show file tree
Hide file tree
Showing 44 changed files with 1,244 additions and 3 deletions.
63 changes: 63 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python package

on:
push:
branches: [ "main" ]
pull_request:

jobs:
build:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[tests]
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Test with pytest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pytest --cov=src
coveralls --service=github
flake8:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[tests]
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --show-source --statistics --exclude src/vba_precompiler/grammar
- name: Static Test with Mypy
run: |
mypy --exclude /grammar/ src/
66 changes: 65 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,66 @@
[![Coverage Status](https://coveralls.io/repos/github/Beakerboy/VBA-Precompiler/badge.png?branch=main)](https://coveralls.io/github/Beakerboy/VBA-Precompiler?branch=main) ![Build Status](https://github.com/Beakerboy/VBA-Precompiler/actions/workflows/python-package.yml/badge.svg)
# VBA-Precompiler
Precompile VBA source files with specified environment values.

## About
The Microsoft VBA language includes a simple precompilation language (Conditional Compilation). This tool allows users to specify environment parameters to convert a conditional-module-body into a preprocessed-module-body.

This software operates as recommended in the [Microsoft VBA Language Specification](https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-CFB/%5bMS-CFB%5d.pdf).

## Requirements
vba_precompiler is tested on python 3.8 and higher.

## Installation
Use the package manager [pip](https://pip.pypa.io/en/stable/) to install VBA-Precompiler.
```
pip install 'vba_precompiler @ git+https://github.com/Beakerboy/VBA-Precompiler@dev'
```

## Getting Started
vba_precompiler will take a specified directory that contains vba source code, and a set of environment values, and produce a set of matching vba source files in which code is excluded as directed by the precomiler directives.

For example, the following
```
Attribute VB_Name = "Input"
#Const TestType="testing"
#If Win16 Or Then
foo = 6
#ElseIf Win32
foo = 7
#EndIf
'Additional VBA code follows
```

Will be transformed to the following:
```
Attribute VB_Name = "Input"
'#Const TestType="testing"
'#If Win16 Then
foo = 6
'#ElseIf Win32
' foo = 7
'#EndIf
'Additional VBA code follows
```
To run the program
```
python vba_precompiler.py [-h] [-s SYSTEM] [-v VERSION] [-o OUTPUT] directory
positional arguments:
directory The source directory.
options:
-h, --help show this help message and
exit
-s, --system System Type, Win16, Win32, Win64, or Mac.
-v, --version VBA version, 6 or 7.
-o, --output output path, defaults to ./build.
examples:
python -m vba_precompiler -s Win32 -v 7 -o ./build32_7 ./project
```

## Tests
The tests directory contains complete unit and functional tests.

## Contributing
Contributions are welcome. Please ensure new features include unit tests to maintain 100% coverage. All code must adhere to the [PEP8 Standards](https://peps.python.org/pep-0008/) for both formatting and naming. Method signatures must be fully annotated.
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ classifiers = [
"Operating System :: OS Independent",
]
dependencies = [
'PyYAML',
'antlr4-python3-runtime',
'antlr4-tools',
'antlr4-vba'
'antlr4_vba',
'python-dateutil',
]
[project.optional-dependencies]
tests = [
Expand All @@ -32,6 +32,7 @@ tests = [
'pytest-cov',
'pytest-mock',
'types-antlr4-python3-runtime',
'types-python-dateutil',
]
[project.urls]
"Homepage" = "https://github.com/Beakerboy/VBA-Precompiler"
Expand Down
1 change: 1 addition & 0 deletions src/vba_precompiler/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# blank
81 changes: 81 additions & 0 deletions src/vba_precompiler/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import argparse
import sys
from pathlib import Path
from vba_precompiler.compiler import Compiler


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("-s", "--system", default="Win16",
help="Mac, Win16, Win32, or Win64")
parser.add_argument("-v", "--version", default="6",
help="VBA version, 6 or 7")
parser.add_argument("-o", "--output", default="./build",
help="Output directory")
parser.add_argument("directory", default='.',
help="The source directory.")
args = parser.parse_args()
path = Path(args.directory).resolve()
file_list = find_files(path)
win16 = False
win32 = False
win64 = False
mac = False
vba6 = False
vba7 = False
if args.system == "Mac":
mac = True
elif args.system == "Win16":
win16 = True
elif args.system == "Win32":
win32 = True
elif args.system == "Win64":
win32 = True
win64 = True
else:
raise Exception("OS Type Unsupported: " + args.system)
if args.version == "6":
if win32 or mac:
vba6 = True
elif args.version == "7":
vba7 = True
else:
raise Exception("VBA Version Unsupported: " + args.version)

env = {"WIN16": win16, "WIN32": win32, "WIN64": win64,
"MAC": mac, "VBA6": vba6, "VBA7": vba7, "MAC_OFFICE_VERSION": 0}
compiler = Compiler(env)
Path(args.output).mkdir(parents=True, exist_ok=True)
num_errors = 0
for file_name in file_list:
new_file_rel_path = Path(file_name).relative_to(path)
output_path = Path(args.output).resolve()
new_path = output_path.joinpath(new_file_rel_path)
try:
result = compiler.compile(file_name)
except Exception as e:
print("File Failed: " + str(file_name), file=sys.stderr)
print(str(e), file=sys.stderr)
num_errors += 1
else:
new_path.parent.mkdir(parents=True, exist_ok=True)
with new_path.open(mode='a') as fi:
fi.write(result)
if num_errors > 0:
exit_code = 1
sys.exit(exit_code)


def find_files(path: Path) -> list:
"""
Find all gives of given types within a directory
"""
files = []
for child in path.rglob("*"):
if child.suffix in [".bas", ".cls", ".frm"]:
files.append(child)
return files


if __name__ == '__main__':
main()
40 changes: 40 additions & 0 deletions src/vba_precompiler/compiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from antlr4 import CommonTokenStream, FileStream
from pathlib import Path
from typing import TypeVar
from antlr4_vba.vba_ccLexer import vba_ccLexer as Lexer
from antlr4_vba.vba_ccParser import vba_ccParser as Parser
from vba_precompiler.precompiler_visitor import PrecompilerVisitor as Visitor


T = TypeVar('T', bound='Compiler')


class Compiler:
# class default constructor
def __init__(self: T, environment: dict) -> None:
self.environment = environment

def compile(self: T, path: str) -> str:
if Path(path).exists():
input_stream = FileStream(path)
lexer = Lexer(input_stream)
else:
raise Exception('file does not exist: ' + path)
ts = CommonTokenStream(lexer)
parser = Parser(ts)
program = parser.startRule()
visitor = Visitor()
visitor.env = self.environment.copy()
lines = visitor.visit(program)
i = 1
f = open(path, 'r')
code = ""
while True:
line = f.readline()
if not line:
break
if i in lines:
code += "'"
code += line
i += 1
return code

0 comments on commit 4fc4888

Please sign in to comment.