rakish 0.9.01.beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ == Package Overview
2
+
3
+ Rakish is a build system package extending the capability of the ::Rake depedency based task management system.
4
+
5
+ Rakish extends the capabilitiues of ::Rake with build module configuration and management features and a host of useful
6
+ utilites for creating tasks and a facile ability for writing and including custom modules for building different types of output.
7
+
8
+ Several types of modules are included here in the base ::Rakish package for building Java, Jar, Zip, and Apache Tomact War file outputs.
@@ -0,0 +1,448 @@
1
+
2
+ ==Java Project file Samples
3
+
4
+ As I haven't written the real "manual" other than the rdocs on the ruby files so I figure this will help
5
+ This is actual production code for me.
6
+
7
+ Hopefully easy enough to sus out - I am too close to it now to judge :)
8
+ And yes ruby people will hate me 'cause I am am a CamelCasingPerson and not an underbar_word_separartor_person
9
+ and I tend to use it for all "dot delimited" languanges
10
+
11
+ The JavaProjectModule does export some standard targets :compile, :javadoc, :dist, :libs with
12
+
13
+ java.addLibraryTargets(...)
14
+
15
+ ===Global Configuration file used by below samples basically from my current project
16
+
17
+ myDir = File.dirname(__FILE__);
18
+ require "rakish"
19
+ require "rakish/CppProjects.rb";
20
+ require "rakish/JavaProjects.rb";
21
+ require "rakish/IntellijConfig.rb";
22
+ require "#{myDir}/OMS/build-scripts/rakish/ServeletProject.rb"; # a custom module for this project
23
+
24
+ module Rakish # not needed just to avoid having to use full paths for module names etc.
25
+
26
+
27
+ Rakish.Configuration :include=>[ IntellijConfig, JavaProjectConfig] do |cfg|
28
+
29
+ # set up global target dependencies.
30
+
31
+ task :autogen
32
+ task :compile =>[:autogen]
33
+ task :libs =>[:compile]
34
+ task :dist =>[:libs]
35
+ task :deploy=>[:dist] # for tomcat servlets and generating docs etc.
36
+ task :undeploy=>[:libs] # tomcat
37
+ task :cleanRebuild =>[:reallyClean, :dist]
38
+
39
+ myDir = File.dirname(File.expand_path(__FILE__));
40
+
41
+ isWindows = Rakish.HostIsWindows_;
42
+
43
+ log = Rakish.log;
44
+
45
+ # cfg.verbose = true; # default is false
46
+
47
+ cfg.orlandoRoot = File.expand_path("#{myDir}");
48
+
49
+ if(cfg.intellij) # we are running this from an intellij UI
50
+ cfg.buildDir = cfg.intellij.outputPath;
51
+ else
52
+ cfg.buildDir = "#{cfg.orlandoRoot}/build";
53
+ end
54
+
55
+ cfg.resourceDir = "#{cfg.buildDir}/production/.orlando";
56
+
57
+ # Java configuration items.
58
+
59
+ cfg.java_home = ENV['JAVA_HOME'];
60
+ cfg.ant_home = ENV['ANT_HOME'] || '/usr/share/ant';
61
+ # cfg.catalina_home = ENV['CATALINA_HOME'] || '/usr/share/tomcat7';
62
+ java = cfg.java;
63
+ java.addJarSearchPath("#{myDir}/third-party-jars");
64
+ java.addClassPaths( 'log4j-1.2.17.jar' ); # everything will inherit this
65
+
66
+ # tomcat deployment options
67
+
68
+ tomcatConfig = BuildConfig.new
69
+ tomcatConfig.enableNewFields do |cfg|
70
+ cfg.managerURL = "http://localhost:8080/manager/text";
71
+ cfg.managerUsername = "for me only";
72
+ cfg.managerPassword = "for me only";
73
+ end
74
+
75
+ # deployment configurations for the three categories of apps
76
+ cfg.omsConfig = BuildConfig.new(tomcatConfig) do |cfg|
77
+ cfg.managerURL = "http://localhost:8080/manager/text"
78
+ end
79
+
80
+ cfg.routerConfig = BuildConfig.new(tomcatConfig) do |cfg|
81
+ cfg.managerURL = "http://localhost:8081/manager/text"
82
+ end
83
+
84
+ (cfg.servicesConfig = BuildConfig.new(tomcatConfig)).enableNewFields do |cfg|;
85
+ cfg.managerURL = "http://localhost:8082/manager/text";
86
+ end
87
+ end
88
+ end
89
+
90
+ ===Root project file that has all the libraries in it
91
+
92
+ All this is is the dependencies and the reallyClean target
93
+
94
+ myDir = File.dirname(__FILE__);
95
+ require "#{myDir}/build-options.rb";
96
+ require "rakish/RakishProject.rb";
97
+
98
+ Rakish.Project(
99
+ :name=>'libs',
100
+ :dependsUpon=> [
101
+ 'artd-util',
102
+ 'artd-vecmath',
103
+ 'artd-net',
104
+ 'artd-bml-generator',
105
+ 'artd-bml-generator/artd-bmlgen-test',
106
+ 'artd-bml-base',
107
+ 'artd-bml-interpreter',
108
+ 'artd-bml-net',
109
+ 'artd-bml-servlet',
110
+ 'artd-uiscene',
111
+ ]
112
+ ) do
113
+
114
+ task :reallyClean do |t|
115
+ log.info("deleting #{buildDir()}");
116
+ FileUtils.rm_rf("#{buildDir()}");
117
+ end
118
+
119
+ export :reallyClean
120
+
121
+ end
122
+
123
+ ===Project file for a simple java Jar file library from a source folder
124
+
125
+ myDir = File.dirname(__FILE__);
126
+ require "#{myDir}/../build-options.rb";
127
+
128
+ Rakish.Project(
129
+ :includes=>[Rakish::JavaProjectModule],
130
+ :name=>'artd-util',
131
+ :dependsUpon=> [
132
+ ]
133
+ ) do
134
+
135
+ export task :resources => [];
136
+
137
+ java.addSourceRoots("#{projectDir}/src");
138
+
139
+ java.addLibraryTargets();
140
+
141
+ end
142
+
143
+ ===Project file with dependencies and generated code using a custom tool
144
+
145
+ myDir = File.dirname(__FILE__);
146
+ require "#{myDir}/../build-options.rb";
147
+ require "#{myDir}/../artd-bml-generator/BmlGeneratorModule.rb"
148
+
149
+ Rakish.Project(
150
+ :includes=>[Rakish::JavaProjectModule, Rakish::BmlGeneratorModule],
151
+ :name=>'artd-bml-interpreter',
152
+ :dependsUpon=> [
153
+ '../artd-bml-generator', # note it is dependedn on the generator this is also built.
154
+ '../artd-util',
155
+ '../artd-net',
156
+ '../artd-bml-base'
157
+ ]
158
+ ) do
159
+
160
+ export task :resources => [];
161
+
162
+ task :cleanautogen do |t|
163
+ FileUtils.rm_rf "#{projectDir}/gensrc";
164
+ end
165
+
166
+ task :cleanAll => [:clean, :cleanautogen ] do
167
+ FileUtils.rm_rf "#{buildDir()}/production/#{moduleName}";
168
+ end
169
+
170
+ generatedSource = createBmlgenTasks('./bmlsrc/*.xml',
171
+ :outPath=>'./gensrc',
172
+ :defaultPackage=>'com.artd.bml.interfaces');
173
+
174
+ export(task :autogen => generatedSource);
175
+
176
+ task :compile => [:autogen];
177
+
178
+ java.addSourceRoots("#{projectDir}/src");
179
+ java.addSourceRoots("#{projectDir}/gensrc", :generated=>true);
180
+
181
+ java.addProjectOutputClasspaths(
182
+ 'artd-util',
183
+ 'artd-net',
184
+ 'artd-bml-base'
185
+ );
186
+
187
+ java.addLibraryTargets();
188
+
189
+ export (task :reallyClean => [:cleanAll])
190
+
191
+ end
192
+
193
+ ===The custom build module used for the gcode geenration from interface definitions
194
+
195
+ # Note to be included in in Rakish projects as a module
196
+
197
+ require 'rexml/streamlistener'
198
+
199
+ module Rakish
200
+
201
+ module BmlGeneratorModule
202
+
203
+ addInitBlock do |pnt,opts| # this is called when the module is included on a project's class instance
204
+ if(pnt != nil) # pnt parent configration or project
205
+ @java_home = pnt.get(:java_home);
206
+ end
207
+ @java_home ||= File.expand_path(ENV['JAVA_HOME']);
208
+ end
209
+
210
+ class XMLListener
211
+ include Rakish::Util
212
+ include REXML::StreamListener
213
+
214
+ attr_accessor(:defaultPackage)
215
+ attr_accessor(:javaOutputDir)
216
+ attr_reader(:javaOutputFiles)
217
+
218
+
219
+ @@outPath = [ 'BMLGEN', 'Interface' ];
220
+
221
+ def initialize()
222
+ @tagPath=[];
223
+ @skipping=nil;
224
+ @javaOutputFiles=FileSet.new();
225
+ end
226
+
227
+ def tag_start(name, attributes)
228
+ @tagPath.push(name);
229
+
230
+ if(@tagPath.length === @@outPath.length && @tagPath[1] === @@outPath[1])
231
+ javaFileName = attributes['name'];
232
+ package = attributes['java.package'] || defaultPackage;
233
+ packagePath = package.gsub('.','/');
234
+ javaOutputFiles.add("#{javaOutputDir}/#{packagePath}/#{javaFileName}.java");
235
+ end
236
+ end
237
+ def tag_end(name)
238
+ @tagPath.pop;
239
+ end
240
+ end
241
+
242
+ def createBmlgenTasks(*xmlsource)
243
+
244
+ opts = (Hash === xmlsource.last) ? xmlsource.pop : {}
245
+
246
+ opts[:outPath]||="./gensrc";
247
+ defaultPackage = opts[:defaultPackage]||="com.artd.bml.interfaces";
248
+ xmlIncludePath = opts[:xmlIncludePath]||=[];
249
+
250
+ outdir = File.expand_path(opts[:outPath]);
251
+
252
+ files = FileSet.new;
253
+ files.include(xmlsource);
254
+
255
+ xmlIncludePath.map! do |path|
256
+ path = File.expand_path(path);
257
+ path
258
+ end
259
+
260
+ xmlsources = [];
261
+ classname = "";
262
+
263
+ listener = XMLListener.new();
264
+ listener.defaultPackage = defaultPackage;
265
+ listener.javaOutputDir = outdir;
266
+
267
+ # collect up all java files this invocation of the generator will create
268
+ # from top level "Interface" tags in all the XML source files.
269
+
270
+ files.each do |srcname|
271
+ # puts("creating task for #{srcname}");
272
+ parser = REXML::Parsers::StreamParser.new(File.new(srcname), listener)
273
+ parser.parse
274
+ xmlsources << srcname;
275
+
276
+ end
277
+
278
+ # create one task for the combined compile for all the xml source files
279
+
280
+ batchTask = Rake::Task::define_unique_task;
281
+ batchTask.enhance("#{binDir()}/artd-bml-generator.jar"); # make sure this is built
282
+
283
+ ensureDirectoryTask(outdir);
284
+ batchTask.enhance([outdir]);
285
+
286
+ batchTask.enhance(batchTask.sources=xmlsources) do |t|
287
+ cfg = t.config;
288
+ FileUtils.cd cfg.projectDir do
289
+ args = " -java javadir=#{outdir} java.package=#{defaultPackage}";
290
+ if(xmlIncludePath.length > 0)
291
+ args += " \"src.includepath=#{xmlIncludePath.join(";")}\"";
292
+ end
293
+ sourcexml = "\"#{t.sources.join("\" \"")}\"";
294
+
295
+ cmd = "\"#{java_home}/bin/java\" -cp \"#{cfg.binDir}/artd-bml-generator.jar\" com.artd.bml.CodeGenerator.XMLToCode #{args} #{sourcexml}";
296
+ system(cmd);
297
+ end
298
+ end
299
+ batchTask.config = self;
300
+
301
+ # create a task for each output java file dependent on the source files.
302
+ # if any java file is out of date invoke the combined task for all the java files once.
303
+
304
+ tasks = [];
305
+
306
+ listener.javaOutputFiles.each do |javaFile|
307
+ tsk = Rake::FileTask.define_task javaFile => [xmlsources] do |t|
308
+ genTask = t.config;
309
+ genTask.invoke();
310
+ end
311
+ tsk.config = batchTask; # note this used to pass in task to be completed
312
+ tasks << tsk;
313
+ end
314
+
315
+ task :clean do
316
+ addCleanFiles(tasks);
317
+ end
318
+
319
+ if(tasks.length > 0)
320
+ task :autogen => tasks;
321
+ end
322
+
323
+ tasks # pass back the list of tasks for creating the java files.
324
+ end
325
+
326
+ end
327
+
328
+ end # Rakish
329
+
330
+ ===A project that builds a tomcat war file
331
+
332
+ Though the ServletProjectModule isn't in the gem (yet) I put this here to
333
+ show building an archive (.war file) and searching the jar file library path to
334
+ get them. moduleJars are built bt dependent projects and they are in the "binDir" the others come
335
+ from the library search path.
336
+
337
+ myDir = File.dirname(__FILE__);
338
+ require "#{myDir}/../build-options.rb";
339
+ require "rakish/TomcatProjects.rb";
340
+
341
+ Rakish.Project(
342
+ :includes=>[Rakish::ServeletProjectModule], # note not in gem at present
343
+ :name=>'omp-router',
344
+ :dependsUpon=> [
345
+ '../../libs',
346
+ '../omp-servlet',
347
+ '../oms-Interfaces',
348
+ '../omp-smpp'
349
+ ]
350
+ ) do
351
+
352
+ export task :resources => [];
353
+
354
+ java.addSourceRoots("#{projectDir}/src");
355
+
356
+ java.addClassPaths("servlet-api.jar");
357
+
358
+ java.addProjectOutputClasspaths(
359
+ 'artd-util',
360
+ 'artd-net',
361
+ 'artd-bml-base',
362
+ 'artd-bml-interpreter',
363
+ 'artd-bml-servlet',
364
+ 'oms-Interfaces',
365
+ 'omp-servlet',
366
+ 'omp-smpp'
367
+ );
368
+
369
+ javac = java.javacTask
370
+
371
+ moduleJars = [
372
+ 'artd-util.jar',
373
+ 'artd-net.jar',
374
+ 'artd-bml-servlet.jar',
375
+ 'artd-bml-interpreter.jar',
376
+ 'artd-bml-net.jar',
377
+ 'artd-bml-generator.jar',
378
+ 'artd-bml-base.jar',
379
+ 'oi-xml.jar',
380
+ 'omp-smpp.jar',
381
+ 'omp-net.jar',
382
+ 'oms-Dao.jar',
383
+ 'oms-Connector.jar',
384
+ 'omp-servlet.jar',
385
+ 'oms-Protocol.jar',
386
+ 'oms-Interfaces.jar'
387
+ ].map! { |name| File.join(binDir(), name); };
388
+
389
+ libJars = java.resolveJarsWithPath([
390
+ 'SMPPLibrary.jar',
391
+ 'commons-logging-1.1.3.jar',
392
+ 'commons-logging-api-1.1.jar',
393
+ 'commons-validator-1.3.1.jar',
394
+ 'displaytag-1.2.jar',
395
+ 'dom4j-1.6.1.jar',
396
+ 'freemarker-2.3.19.jar',
397
+ 'hibernate-c3p0-4.1.8.final.jar',
398
+ 'hibernate-commons-annotations-4.0.5.Final.jar',
399
+ 'hibernate-core-4.3.8.Final.jar',
400
+ 'antlr-2.7.7.jar',
401
+ 'hibernate-jpa-2.1-api-1.0.0.Final.jar',
402
+ 'antlr-runtime-3.0.1.jar',
403
+ 'jandex-1.1.0.Final.jar',
404
+ 'javassist-3.11.0.GA.jar',
405
+ 'javassist-3.18.1-GA.jar',
406
+ 'javax.mail-1.4.5.jar',
407
+ 'jboss-logging-3.1.3.GA.jar',
408
+ 'jboss-logging-annotations-1.2.0.Beta1.jar',
409
+ 'jboss-transaction-api_1.2_spec-1.0.0.Final.jar',
410
+ 'jta-4.0.jar',
411
+ 'asm-3.2.jar',
412
+ 'log4j-1.2.17.jar',
413
+ 'asm-attrs.jar',
414
+ 'ognl-3.0.6.jar',
415
+ 'c3p0-0.9.1.jar',
416
+ 'org-apache-commons-logging.jar',
417
+ 'cglib-2.1.96.jar',
418
+ 'slf4j-api-1.7.10.jar',
419
+ 'commons-fileupload-1.3.1.jar',
420
+ 'slf4j-log4j12-1.7.10.jar',
421
+ 'commons-httpclient-3.1.jar',
422
+ 'struts2-core-2.3.20.jar',
423
+ 'commons-io-2.2.jar',
424
+ 'struts2yuiplugin-0.1-ALPHA-7.jar',
425
+ 'commons-lang-2.4.jar',
426
+ 'xwork-core-2.3.20.jar',
427
+ 'commons-lang3-3.2.jar'
428
+ ],
429
+ :errorOnMissing=>true # if one is not found this will raise an exception.
430
+ );
431
+
432
+ warBuilder = createWarBuilder
433
+
434
+ warBuilder.addFiles('/WEB-INF/lib', moduleJars, libJars );
435
+ warBuilder.addFileTree('/WEB-INF/classes', java.outputClasspath, "#{java.outputClasspath}/**/*" );
436
+ warBuilder.addFileTree('/', './web', './web/WEB-INF/**/*');
437
+
438
+ warFile = warBuilder.warFileTask "#{binDir()}/omp-router-0.1-dev.war" => [ :compile, :libs ];
439
+
440
+ export task :dist => [ warFile ];
441
+
442
+ createDeploymentTasks( routerConfig,
443
+ :sourceWar=>warFile.name,
444
+ :webPath=>"a.l" );
445
+
446
+
447
+ end
448
+
data/doc/ToDoItems.txt ADDED
@@ -0,0 +1,18 @@
1
+
2
+ ==Items To Do
3
+
4
+ ===Of course make a decent C++/C/asm module for msc and gcc g++ etc
5
+
6
+ ===Make a usable MultiProcessTask for spawning multiple asynchronous tasks
7
+ The one in Rake actually slows things down when too
8
+ many are spawned due to the thread swithing overhead so it really isn't practical. What it should do is maintain a semaphone
9
+ managed queue and only spawn as many tasks as there are processor cores on the machine to handle the big processes they
10
+ spawn ( compilers and the like ) the little ruby tasks are better off being run in the primary thread unless they are
11
+ doing something with a lot of waiting involved like uploading a file. In any case limiting the number of threads
12
+ would help this too.
13
+
14
+ ===Make tomcat servlet module releasable
15
+ ===A real manual
16
+ ===Make intellij integration module releasable
17
+
18
+
data/doc/UserGuide ADDED
@@ -0,0 +1,15 @@
1
+
2
+ = Rakish - Build system user guide
3
+
4
+
5
+ :include:doc/RakishOverview.html
6
+
7
+ ===See why I did it at the end if curious.
8
+
9
+ :include:doc/SimpleJavaSamples.txt
10
+
11
+ :include:doc/WhyIDidIt.txt
12
+
13
+ :include:doc/ToDoItems.txt
14
+
15
+
data/doc/WhyIDidIt.txt ADDED
@@ -0,0 +1,96 @@
1
+
2
+ ==Why I did it
3
+
4
+ I have over many years had to use various build scripting systems IDEs and wiite scripts for them.
5
+
6
+ Many projects after they got at all big required lots of things to be done,
7
+ - signing files,
8
+ - building custom tools
9
+ - genrating code with those custom tools
10
+ - auto generating version Ids
11
+ - generating documentation
12
+ - inserting special comment headers
13
+ - creating distribution packages
14
+ - invoking automated testing.
15
+ - build on checkin on various build servers
16
+
17
+ And many other little things.
18
+
19
+ Makefiles though I managed to do what was needed are arcane,
20
+ don't handle strings with spaces in them, have no sense of arrays or collections,
21
+ and really don't handle large trees of dependent modules well having to reload
22
+ everything in every subprocess and re-scan all the dependencies each time.
23
+ and one has to deal with backslash hell and the differences beween windows and
24
+ unix shells. and there is no sense at all of scoped variables or namespaces. I could go on :)
25
+
26
+ That said the big advantage was it is open, available on all platforms, and either part of the os or
27
+ brain dead easy to install. The goal being tell a new team member "download this", "install make"
28
+ go to the root folder and enter "make all" ir whatever and it builds the whole massive project.
29
+
30
+ That all said makefiles have been superior at divvying up processes such as C++
31
+ compiles efficiently on multi-core machines for the absolute fastest rebuild build times.
32
+ However the big projects were surely an undocumented hell for anyone trying to figure
33
+ out how all the makefiles were put together.
34
+
35
+ With Java I got really annoyed with Ant as XML is just not a programming system.
36
+
37
+ And yes I know ther are all sorts of Mavens, IDEs and then like. Many either platform proprietary
38
+ or so oriented to a particular language like Ant with Java that builing and manging mixeld lanquage
39
+ projects with them was a bear. And I found that one always ended up havint to write programs in
40
+ other languanges or scripting systems to make it all hang together and one eneded up with a hard
41
+ to document and mantain blob of stuff again.
42
+
43
+ So maybe 8 years ago I decided I woudl bite the bullet and make what I wanted in a "makefile" system.
44
+ not dependent an a particular IDE or proprietary product or using a limited specialty languange etc.
45
+
46
+ I discovered Ruby and it's sistem module Rake which are very nice. Ruby is coherant, well documented,
47
+ standard, cross platform, easy to install, has great text processing capabilities, regexes, collections, namespaces,
48
+ scoped variables, hashes "closures - ie: blocks" and many other great features. Easier to manage that ECMA
49
+ script etc. Anyway you Ruby people will know what I mean.
50
+
51
+ Rake though it covers the basics of "makefiles" is limited in scope but does handle task management and dependency,
52
+ checking well and has namespaces and useful utilities for paths and files. And it is built on Ruby.
53
+
54
+ So Rakish as tha name inplies is my attempt at making Rake a bit sexier.
55
+
56
+ Using it all I have "Project"s which are the items used to define medium build atoms larger than tasks.
57
+ A library from source, a jar file, a set of classes, etc and contauined in a single folder.
58
+ And then projects are dependent on other projects. Each project has it's own internal
59
+ namespece so internal tasks unless "exported" don't conflict with others outside.
60
+ Also I can load the entire tree of projects in one go fast and can check dependencies globally on the whole
61
+ mess for faster incremental build times and a tree of makefiles. Project loading s lazy so if one builds
62
+ a module with few dependencies only what it is dependent on will be loaded.
63
+
64
+ The "Configurations" are managed through a "PropertyBag" object I built from ruby. Property bags can have fields
65
+ added dynamically, and can inherit values from "parents" and what I might call "uncles" ( parents are inherited by
66
+ their children. "Uncles" are only available at the level they are attached to and are not passed down
67
+ to children at the next level. This enables a configuration override structure so for example each project
68
+ though it might inherit the global intermediate output path appends it's name etc so it has it's own sandbox.
69
+
70
+ I also made a superclass constructor system so multiple modules included in Projects are intialized
71
+ on an instance of a project in much the same way as Java or C++ superclasses, yet they are still mixins
72
+ offering nice multiple inheritance.
73
+
74
+ "Projects" are basiclally property bags and hosts for these modules, depedency initialization code and
75
+ containers and builders of tasks. Projects include modules that customize the projects for different types of
76
+ capabilities. One can add custom capabilites to any project by writing a module for it to the pattern of the others.
77
+
78
+ The ArchiveBuilder (Zip and Jar modules ) and the Java one in the gem is mature and tested. I re-did it as a
79
+ pattern of how I now think best to structure these modules. It consists of a configuration Module and a
80
+ Project module. I am now using it for my current Java projects integrated with intellij.
81
+
82
+ There is a C++ module I converted from old makefiles but it is
83
+ not really ready for prime time as it has too many messy dependencies on the local enviropnment just like the makefiles
84
+ it is included more just so people know it is there. And I have a tomcat servlet module and a few others.
85
+ I would have improved it but Java web programs has been the majority of work for the past several years and the
86
+ C++ stuff still works to build my libraries and native code for Java bindings.
87
+
88
+ I have glue modules to run these builds integrated with intellij, have done so with eclipse, and Microsoft
89
+ Developers Studio.
90
+
91
+ Anyway maybe this can grow into a community resource so I can use it and clients won't be afraid of it :)
92
+ And maybe convice people I am worth hiring :)
93
+
94
+ All the best
95
+ Peter K.
96
+