Skip to content
Open
1 change: 1 addition & 0 deletions converter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.coverage
24 changes: 24 additions & 0 deletions converter/docs/build.ninja
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
flags = -O3

pool_depth = 4

pool heavy_object_pool
depth = pool_depth

rule compile
command = g++ $flags -c $in -o $out
pool = heavy_object_pool

rule link
command = g++ $in -o $out

build hello.o: compile hello.cc
flags = -O2
pool = console

build hello: link hello.o

build dummy: phony

default hello
default foo1 foo2
51 changes: 51 additions & 0 deletions converter/docs/build.ninja.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#include "manifest.h"

using namespace shadowdash;

void manifest() {
let(flags, "-O3");

let(pool_depth, "4");

auto heavy_object_pool = pool_(bind(depth, "pool_depth"_v));

auto compile = rule( {
bind(command, "g++", "flags"_v, "-c", in, "-o", out),
bind(pool, "heavy_object_pool"_v)
} );

auto link = rule( {
bind(command, "g++", in, "-o", out),
} );

auto build_c = build(list{ str{ "hello.o" } },
{},
compile,
list{ str{ "hello.cc" } },
{},
{},
{ bind(flags, "-O2"),
bind(pool, "console"_v) }
);

auto build_l = build(list{ str{ "hello" } },
{},
link,
list{ str{ "hello.o" } },
{},
{},
{}
);

auto build_p = build(list{ str{ "dummy" } },
{},
phony,
{},
{},
{},
{}
);

default_(str{ "hello" });
default_(list{ str{ "foo1" }, str{ "foo2" } });
}
61 changes: 61 additions & 0 deletions converter/src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Usage
```
usage: converter.py [-h] input_file output_file

Converts ninja files to ShadowDash manifest files.

positional arguments:
input_file Path to the input file to process
output_file Path to the output file

options:
-h, --help show this help message and exit
```

You can call it using the following command:
```bash
python3 converter.py input_file output_file
```

# Prerequisites
- Python 3.6 or higher
- Testing
- pytest
- pytest-mock
- pytest-cov (optional)

# Test

## Unit Tests

The unit tests are located in the `tests` directory.
It requires the `pytest` and `pytest-mock` packages to run the tests.

To run the unit tests, you can use the following command:
```bash
# from the converter directory
# ./converter
pytest
```

Or with coverage report (requires the `pytest-cov` package):
```bash
# from the converter directory
# ./converter
pytest --cov=src --cov-report=term
```

## Factual Tests

To test the program we can use the files in the `docs` folder. The following command will convert the `build.ninja` file to a `build.ninja.cc` file:
```bash
# from the converter directory
# ./converter
python3 -m src.converter ./docs/build.ninja build.ninja.cc
```
Aside from formatting, the file `build.ninja.cc` should be identical to the `build.ninja.cc` file in the docs directory.


# Implementation

The program is implemeted to match the languages developed by the `ninja` build system and Team 4 `new-language` subteam.
1 change: 1 addition & 0 deletions converter/src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# this file is intentionally left blank to mark the directory as a package
25 changes: 25 additions & 0 deletions converter/src/builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import List
from .token import Token

class Builder:
"""
Builder class to generate the content of a ShadowDash manifest file from tokens.
"""

@staticmethod
def get_file_content(tokens: List[Token]) -> str:
"""
Generates the content of the ShadowDash manifest file.

Args:
tokens (List[Token]): List of tokens to be converted.

Returns:
str: Content of the ShadowDash manifest file.
"""
# Start with the necessary includes and namespace
c = '#include "manifest.h"\n\nusing namespace shadowdash;\n\nvoid manifest() {\n\n'
# Convert each token to its string representation and join them
c += "\n\n".join([token.get_string() for token in tokens])
c += "\n\n}"
return c
38 changes: 38 additions & 0 deletions converter/src/converter.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The files & coding generally have appropriate granularity, though some functions could be further decomposed. For example, the main function can be updated to decouple the file handling from token parsing and file writing, it would allow main() to act as a lightweight wrapper and keeps processing logic reusable.

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import argparse
import os
from typing import List
from .builder import Builder
from .parser import Parser
from .token import Token

def main() -> None:
"""
Main function to convert a ninja build file to a ShadowDash manifest file.
"""
parser = argparse.ArgumentParser(description='Converts ninja files to ShadowDash manifest files.')

# Define input and output file arguments
parser.add_argument('input_file',
help='Path to the input file to process')
parser.add_argument('output_file',
help='Path to the output file')

args = parser.parse_args()

# Check if the input file exists
if not os.path.isfile(args.input_file):
print(f"Error: The file {args.input_file} does not exist.")
exit(1)

# Tokenize the input file
tokenslist: List[Token] = Parser.tokenize(args.input_file)

# Write the converted content to the output file
with open(args.output_file, 'w') as outfile:
content: str = Builder.get_file_content(tokenslist)
outfile.write(content)

print(f"File {args.input_file} converted to {args.output_file}")

if __name__ == '__main__':
main()
52 changes: 52 additions & 0 deletions converter/src/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import re
from typing import List
from .token import Token, Rule, Pool, Bind, Assign, Build, Default

class Parser:
"""
Parser class to tokenize a ninja build file.
"""

@staticmethod
def tokenize(path: str) -> List[Token]:
"""
Tokenizes the given ninja build file.

Args:
path (str): Path to the ninja build file.

Returns:
List[Token]: List of tokens parsed from the file.
"""
tokenlist = []
# Regex pattern to match lines that are not indented and their subsequent indented lines
pattern = re.compile(r"(?P<group>^[^\t\s].*(?:\n[ \t]+.*)*)", re.MULTILINE)
with open(path, "r") as ninjafile:
matches = pattern.findall(ninjafile.read())
for match in matches:
tokenlist.append(Parser._get_token(match))
return tokenlist

@staticmethod
def _get_token(command: str) -> Token:
"""
Converts a command string to a Token object.

Args:
command (str): Command string from the ninja build file.

Returns:
Token: Corresponding Token object.
"""
if command.startswith("rule"):
return Rule(command)
elif command.startswith("pool "):
return Pool(command)
elif command.startswith("build "):
return Build(command)
elif command.startswith("bind"):
return Bind(command)
elif command.startswith("default"):
return Default(command)
else:
return Assign(command)
Loading