Skip to content

Commit fd3ecb5

Browse files
committed
updating builder-tool
1 parent 420da52 commit fd3ecb5

3 files changed

Lines changed: 160 additions & 89 deletions

File tree

.github/workflows/OnRelease.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
run: cd builder-tool && pip install -r requirements.txt
2424

2525
- name: Build
26-
run: cd builder-tool && python pack.py --dist-dir dist -pv ${{ github.event.release.tag_name }} --debug
26+
run: cd builder-tool && python pack.py -pv ${{ github.event.release.tag_name }} --debug
2727

2828
- name: Publish
2929
uses: AButler/upload-release-assets@v3.0

builder-tool/pack.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from src.builderTool import BaseBuilder, Logger, PYTHON, NULL_TARGET #this file use the module to build itself
2-
import os
32
import shutil
43

54

@@ -14,10 +13,9 @@ def Setup(self):
1413
Logger.debug('Copying readme.md')
1514
self.CopyAndReplaceByPackageVersion('readme.md', self.tempDir + '/readme.md')
1615

16+
1717
def Build(self):
18-
command = f'{PYTHON} -m build --outdir {self.distDir} {self.tempDir} > {NULL_TARGET}'
19-
Logger.debug('Executing command: ' + command)
20-
os.system(command)
18+
self.runCommand(f'{PYTHON} -m build --outdir {self.distDir} {self.tempDir} > {NULL_TARGET}')
2119

2220

2321
BaseBuilder.execute()

builder-tool/src/builderTool.py

Lines changed: 157 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,55 @@
11
import argparse
2-
from gamuLogger import Logger, LEVELS
3-
42
import os, sys, shutil
3+
from enum import Enum
4+
from tempfile import mkdtemp, mkstemp
5+
6+
# some useful constants
7+
8+
PYTHON = sys.executable
9+
NULL_TARGET = '/dev/null' if os.name == 'posix' else 'nul'
510

6-
from tempfile import mkdtemp
11+
try:
12+
from gamuLogger import Logger, LEVELS, debugFunc
13+
except ImportError:
14+
print("Logger not found, installing...", end=' ', flush=True)
15+
os.system(f'{sys.executable} -m pip install https://github.com/GamuNetwork/logger/releases/download/2.0.0-alpha.4/gamu_logger-2.0.0a4-py3-none-any.whl > {NULL_TARGET} 2> {NULL_TARGET}')
16+
print("done")
17+
from gamuLogger import Logger, LEVELS, debugFunc
718

819
class BaseBuilder:
20+
"""
21+
Create a new builder by subclassing this class and implementing the steps as methods
22+
steps are:
23+
- Setup
24+
- Build
25+
- Tests
26+
- Docs
27+
- Publish
28+
29+
example:
30+
```python
31+
class Builder(BaseBuilder):
32+
def Setup(self):
33+
# do something
34+
35+
def Build(self):
36+
# do something
37+
38+
BaseBuilder.execute() #this will run the steps
39+
```
40+
41+
Use `python {your_script}.py -h` to see the available options
42+
"""
43+
class Status(Enum):
44+
WAITING = 0
45+
RUNNING = 1
46+
FINISHED = 2
47+
FAILED = 3
48+
DISABLED = 4
49+
50+
def __str__(self):
51+
return self.name
52+
953
def __init__(self):
1054
if self.__class__ == BaseBuilder:
1155
raise Exception('BaseBuilder is an abstract class and cannot be instantiated')
@@ -21,28 +65,41 @@ def __init__(self):
2165
buildersOptions.add_argument('--no-build', action='store_true', help='Do not build the package')
2266
buildersOptions.add_argument('--publish', action='store_true', help='Publish the package')
2367
buildersOptions.add_argument('--no-clean', action='store_true', help='Do not clean temporary files')
24-
buildersOptions.add_argument('--temp-dir', help='Temporary directory', type=str, default=mkdtemp())
25-
buildersOptions.add_argument('--dist-dir', help='Distribution directory', type=str, default='dist')
26-
buildersOptions.add_argument('-pv', '--package-version', help='Package version', type=str, default='0.0.0')
68+
buildersOptions.add_argument('--temp-dir', help='Temporary directory (used to generate the package)', type=str, default=mkdtemp())
69+
buildersOptions.add_argument('--dist-dir', help='Distribution directory (where to save the built files)', type=str, default='dist')
70+
buildersOptions.add_argument('-pv', '--package-version', help='set the version of the package you want to build', type=str, default='0.0.0')
2771

2872
self.argumentParser.add_argument('--version', '-v', action='version', version='%(prog)s 1.0')
2973

3074
self.args = self.argumentParser.parse_args()
3175

76+
self.__steps = {
77+
"Setup": self.Status.WAITING,
78+
"Build": self.Status.DISABLED if self.args.no_build else self.Status.WAITING,
79+
"Tests": self.Status.DISABLED if self.args.no_tests else self.Status.WAITING,
80+
"Docs": self.Status.DISABLED if self.args.no_docs else self.Status.WAITING,
81+
"Publish": self.Status.WAITING if self.args.publish else self.Status.DISABLED
82+
}
83+
84+
self.clean = not self.args.no_clean
85+
86+
self.__remainingSteps = len([step for step in self.__steps if self.__steps[step] != self.Status.DISABLED])
87+
88+
self.__stepDependencies = {
89+
"Setup": [],
90+
"Build": ["Setup"],
91+
"Docs": ["Setup"],
92+
"Tests": ["Setup", "Build"],
93+
"Publish": ["Setup", "Build", "Tests", "Docs"]
94+
}
95+
3296
if self.args.debug:
3397
Logger.setLevel('stdout', LEVELS.DEBUG)
3498

3599

36100
Logger.debug('Using temporary directory: ' + os.path.abspath(self.args.temp_dir))
37101
Logger.debug('Using distribution directory: ' + os.path.abspath(self.args.dist_dir))
38102

39-
if os.path.exists(self.args.temp_dir):
40-
Logger.debug('Removing temp directory for cleaning')
41-
shutil.rmtree(self.args.temp_dir)
42-
43-
Logger.debug('Creating temp directory')
44-
os.makedirs(self.args.temp_dir)
45-
46103
@property
47104
def tempDir(self):
48105
return os.path.abspath(self.args.temp_dir)
@@ -62,79 +119,96 @@ def CopyAndReplaceByPackageVersion(self, src, dst, versionString = "{version}"):
62119
with open(dst, 'w') as file:
63120
file.write(content)
64121

65-
66-
67-
# These methods should be overriden by the child class
68-
def Setup(self):
69-
"""Copy required file to the temporary directory and replace the version string by the package version, install dependencies, etc."""
70-
Logger.warning('no setup configured')
71-
72-
def Tests(self):
73-
"""Run the tests for the package"""
74-
Logger.debug('no tests configured')
75-
76-
def Docs(self):
77-
"""Generate the documentation for the package"""
78-
Logger.debug('no docs configured')
79-
80-
def Build(self):
81-
"""Build the package"""
82-
Logger.warning('no build configured')
83-
84-
def Publish(self):
85-
"""Publish the package"""
86-
Logger.error('no publish configured')
87-
88-
89-
def __clean(self):
90-
shutil.rmtree(self.args.temp_dir)
91-
92-
93-
def __run(self):
94-
"""
95-
Order of execution:
96-
- Setup
97-
- Build
98-
- Tests
99-
- Docs
100-
- Publish
101-
- Clean
102-
103-
If any of this steps fails, the process should stop, and the clean method should be called
104-
"""
105-
106-
Logger.info('Running setup')
107-
self.Setup()
108-
Logger.debug('Setup finished')
109-
110-
if not self.args.no_build:
111-
Logger.info('Building package')
112-
self.Build()
113-
114-
if self.args.no_tests:
115-
Logger.warning('Skipping tests')
122+
def runCommand(self, command) -> bool:
123+
Logger.debug('Executing command: ' + command)
124+
stdoutFile, stdoutPath = mkstemp()
125+
stderrFile, stderrPath = mkstemp()
126+
returnCode = os.system(f'{command} > {stdoutPath} 2> {stderrPath}')
127+
if returnCode != 0:
128+
Logger.error(f'Task failed successfully with return code {returnCode}') # this is for the joke
129+
with open(stdoutPath, 'r') as file:
130+
Logger.debug('stdout: ' + file.read())
131+
with open(stderrPath, 'r') as file:
132+
Logger.debug('stderr: ' + file.read())
133+
134+
os.remove(stdoutPath)
135+
os.remove(stderrPath)
136+
return False
116137
else:
117-
Logger.info('Running tests')
118-
self.Tests()
138+
Logger.debug('Command executed successfully')
139+
os.remove(stdoutPath)
140+
os.remove(stderrPath)
141+
return True
119142

120-
if not self.args.no_docs:
121-
Logger.info('Generating documentation')
122-
self.Docs()
123143

124-
if self.args.publish:
125-
Logger.info('Publishing package')
126-
self.Publish()
144+
def __clean(self) -> bool:
145+
Logger.info('Cleaning temporary directory')
146+
try:
147+
shutil.rmtree(self.args.temp_dir)
148+
except Exception as e:
149+
Logger.error('Error while cleaning temp directory: ' + str(e))
150+
return False
127151
else:
128-
Logger.info('Package not published')
152+
Logger.debug('Temporary directory cleaned')
153+
return True
129154

130-
if self.args.no_clean:
131-
Logger.warning('Skipping cleaning')
155+
156+
def __canStepBeStarted(self, step):
157+
for dependency in self.__stepDependencies[step]:
158+
if self.__steps[dependency] != self.Status.FINISHED:
159+
return False
160+
return True
161+
162+
def __runStep(self, step : str):
163+
'''
164+
A step is considered failed if it raises an exception, or if it returns False
165+
If it returns None, it is considered successful, but raises a warning
166+
'''
167+
hasSucceeded = False
168+
try:
169+
hasSucceeded = getattr(self, step)()
170+
except Exception as e:
171+
return False
132172
else:
133-
Logger.info('Cleaning temporary files')
173+
if hasSucceeded is None:
174+
Logger.warning('Step "' + step + '" did not return a value, but didn\'t throw anything, assuming it has succeeded')
175+
return True
176+
return hasSucceeded
177+
178+
def __run(self, configuredSteps : list[str]):
179+
for step in self.__steps:
180+
if step not in configuredSteps and step != '__clean':
181+
self.__steps[step] = self.Status.DISABLED
182+
self.__remainingSteps -= 1
183+
Logger.debug('Step "' + step + '" disabled')
184+
185+
186+
HasFailed = False
187+
while self.__remainingSteps > 0 and not HasFailed:
188+
for step in self.__steps:
189+
if self.__steps[step] == self.Status.WAITING and self.__canStepBeStarted(step):
190+
Logger.info('Starting step "' + step + '"')
191+
self.__steps[step] = self.Status.RUNNING
192+
193+
hasSucceeded = self.__runStep(step)
194+
195+
if hasSucceeded:
196+
self.__steps[step] = self.Status.FINISHED
197+
self.__remainingSteps -= 1
198+
else:
199+
self.__steps[step] = self.Status.FAILED
200+
Logger.error('Step "' + step + '" failed')
201+
HasFailed = True
202+
break
203+
204+
if self.clean:
134205
self.__clean()
135-
Logger.debug('Temporary files cleaned')
136-
137-
Logger.info('Process finished')
206+
207+
if HasFailed:
208+
Logger.critical('A step has failed')
209+
sys.exit(1)
210+
else:
211+
Logger.info('Build finished successfully')
138212

139213

140214
@staticmethod
@@ -146,10 +220,9 @@ def execute():
146220
elif len(subClasses) > 1:
147221
Logger.critical('Multiple builders found')
148222
sys.exit(1)
149-
subClasses[0]().__run()
150-
223+
224+
possibleSteps = ['Setup', 'Tests', 'Docs', 'Build', 'Publish']
225+
steps = [step for step in subClasses[0].__dict__ if step in possibleSteps]
226+
subClasses[0]().__run(steps)
151227

152-
# some useful constants
153228

154-
PYTHON = sys.executable
155-
NULL_TARGET = '/dev/null' if os.name == 'posix' else 'nul'

0 commit comments

Comments
 (0)