11import argparse
2- from gamuLogger import Logger , LEVELS
3-
42import 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
819class 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