@@ -45,24 +45,36 @@ def extract_zip(mda_file):
4545 zip_file .extractall (temp_dir .name )
4646 return temp_dir
4747
48+ BUILDER_PROCESS = None
49+ def stop_processes ():
50+ if BUILDER_PROCESS is not None :
51+ proc = BUILDER_PROCESS
52+ proc .terminate ()
53+ proc .communicate ()
54+ proc .wait ()
55+
4856def container_call (args ):
4957 build_executables = ['podman' , 'docker' ]
5058 build_executable = None
59+ logger_stdout = None
60+ logger_stderr = None
5161 for builder in build_executables :
52- builder = shutil .which (builder )
53- if builder is not None :
54- build_executable = builder
62+ build_executable = shutil .which (builder )
63+ if build_executable is not None :
64+ logger_stderr = logging .getLogger (builder + '-stderr' )
65+ logger_stdout = logging .getLogger (builder + '-stdout' )
5566 break
5667 if build_executable is None :
5768 raise Exception ('Cannot find Podman or Docker executable' )
5869 proc = subprocess .Popen ([build_executable ] + args , stdout = subprocess .PIPE , stderr = subprocess .PIPE , universal_newlines = True )
70+ BUILDER_PROCESS = proc
5971
6072 sel = selectors .DefaultSelector ()
6173 sel .register (proc .stdout , selectors .EVENT_READ )
6274 sel .register (proc .stderr , selectors .EVENT_READ )
63- logger = logging .getLogger (build_executable )
6475
65- last_line = None
76+ last_line_stdout = None
77+ last_line_stderr = None
6678 stderr_open , stderr_open = True , True
6779 while stderr_open or stderr_open :
6880 for key , _ in sel .select ():
@@ -75,67 +87,137 @@ def container_call(args):
7587 continue
7688 data = data .rstrip ()
7789 if key .fileobj is proc .stdout :
78- last_line = data
79- logger .info (data )
90+ last_line_stdout = data
91+ logger_stdout .info (data )
8092 elif key .fileobj is proc .stderr :
81- logger .error (data )
93+ last_line_stderr = data
94+ # stderr is mostly used for progress notifications, not errors
95+ logger_stderr .info (data )
8296
8397 sel .close ()
98+ BUILDER_PROCESS = None
8499 if proc .wait () != 0 :
85- raise Exception ('Builder returned with error' )
86- return last_line
100+ raise Exception (f"Builder returned with error: { last_line_stderr } " )
101+ return last_line_stdout
102+
103+ def pull_image (image_url ):
104+ try :
105+ container_call (['image' , 'pull' , image_url ])
106+ return image_url
107+ except :
108+ return None
109+
110+ def delete_container (container_id ):
111+ try :
112+ container_call (['container' , 'rm' , '--force' , container_id ])
113+ except Exception as e :
114+ logging .warning ('Failed to delete container {}: {}' .format (container_id , e ))
115+
116+ def build_mpr_builder (mx_version , dotnet , artifacts_repository = None ):
117+ builder_image_tag = f"mxbuild-{ mx_version } -{ dotnet } -{ platform .machine ()} "
118+ builder_image_url = None
119+ if artifacts_repository is not None :
120+ builder_image_url = f"{ artifacts_repository } :{ builder_image_tag } "
121+ image_hash = pull_image (builder_image_url )
122+ if image_hash is not None :
123+ return builder_image_url
87124
88- def build_mpr_builder (mx_version , dotnet ):
89125 prefix = ''
90126 if platform .machine () == 'arm64' and dotnet == 'dotnet' :
91127 prefix = 'arm64-'
92128
93129 mxbuild_filename = f"{ prefix } mxbuild-{ mx_version } .tar.gz"
94130 mxbuild_url = f"https://download.mendix.com/runtimes/{ mxbuild_filename } "
95131
96- # TODO: build image only if it doesn't exist yet
97- return container_call (['build' ,
98- '--build-arg' , f"MXBUILD_DOWNLOAD_URL={ mxbuild_url } " ,\
99- '--file' , f"mxbuild/rootfs-mxbuild-{ dotnet } .dockerfile" ,
100- 'mxbuild' ])
101-
102- def build_mpr (source_dir , mpr_file ):
103- print (f"MPR file { mpr_file } " )
132+ build_args = ['--build-arg' , f"MXBUILD_DOWNLOAD_URL={ mxbuild_url } " ,
133+ '--file' , f"mxbuild/{ dotnet } .dockerfile" ]
134+ if artifacts_repository is not None :
135+ build_args += ['--tag' , builder_image_url ]
136+
137+ image_id = container_call (['image' , 'build' ] + build_args + ['mxbuild' ])
138+ if artifacts_repository is not None :
139+ try :
140+ container_call (['image' , 'push' , builder_image_url ])
141+ except Exception as e :
142+ logging .warning ('Failed to push mxbuild into artifacts repository: {}; continuing with the build' .format (e ))
143+ return image_id
144+
145+ def get_git_commit (source_dir ):
146+ git_head = os .path .join (source_dir , '.git' , 'HEAD' )
147+ if not os .path .isfile (git_head ):
148+ return None
149+ with open (git_head ) as git_head :
150+ git_head_line = git_head .readline ().split ()
151+ if len (git_head_line ) == 1 :
152+ # Detached commit
153+ return git_head_line [0 ]
154+ if len (git_head_line ) > 2 :
155+ return Exception (f"Unsupported Git HEAD format { git_head_line } " )
156+ git_branch = git_head_line [1 ].split ('/' )
157+ git_branch_file = os .path .join (* ([source_dir , '.git' ] + git_branch ))
158+ if not os .path .isfile (git_branch_file ):
159+ return Exception ('Git branch file doesn\' t exist' )
160+ with open (git_branch_file ) as git_branch_file :
161+ return git_branch_file .readline ()
162+
163+
164+ def build_mpr (source_dir , mpr_file , destination , artifacts_repository = None ):
104165 cursor = sqlite3 .connect (mpr_file ).cursor ()
105166 cursor .execute ("SELECT _ProductVersion FROM _MetaData LIMIT 1" )
106167 mx_version = cursor .fetchone ()[0 ]
107168 mx_version_value = parse_version (mx_version )
108- logging .debug ("Detected Mendix version {}" .format (mx_version_value ))
109- if mx_version_value >= (10 , 0 , 0 , 0 ):
110- builder_image = build_mpr_builder (mx_version , 'dotnet' )
111- build_result = container_call (['run' , '--volume' , os .path .abspath (source_dir )+ ':/workdir/project:rw' , builder_image ])
112- else :
113- builder_image = build_mpr_builder (mx_version , 'mono' )
114- build_result = container_call (['run' , '--volume' , os .path .abspath (source_dir )+ ':/workdir/project:rw' , builder_image ])
115- raise Exception ('TODO' )
169+ logging .debug ('Detected Mendix version {}' .format ('.' .join (map (str ,mx_version_value ))))
170+ dotnet = 'dotnet' if mx_version_value >= (10 , 0 , 0 , 0 ) else 'mono'
171+ builder_image = build_mpr_builder (mx_version , dotnet , artifacts_repository )
172+ model_version = get_git_commit (source_dir )
173+ model_version = 'unversioned' if model_version is None else model_version
174+
175+ container_id = container_call (['container' , 'create' , builder_image , os .path .basename (mpr_file ), model_version ])
176+ atexit .register (delete_container , container_id )
177+ container_call (['container' , 'cp' , os .path .abspath (source_dir )+ '/.' , f"{ container_id } :/workdir/project" ])
178+ build_result = container_call (['start' , '--attach' , '--interactive' , container_id ])
179+
180+ temp_dir = tempfile .TemporaryDirectory (prefix = 'mendix-docker-buildpack' )
181+ container_call (['container' , 'cp' , f"{ container_id } :/workdir/output.mda" , temp_dir .name ])
182+ with zipfile .ZipFile (os .path .join (temp_dir .name , 'output.mda' )) as zip_file :
183+ zip_file .extractall (destination )
116184
117185def parse_version (version ):
118186 return tuple ([ int (n ) for n in version .split ('.' ) ])
119187
120- def prepare_mda (source_dir ):
121- mpk_file = find_default_file (source_dir , '.mpk' )
188+ def prepare_destination (destination_path ):
189+ with os .scandir (destination_path ) as entries :
190+ for entry in entries :
191+ if entry .is_dir () and not entry .is_symlink ():
192+ shutil .rmtree (entry .path )
193+ else :
194+ os .remove (entry .path )
195+ project_path = os .path .join (destination_path , 'project' )
196+ os .mkdir (project_path , 0o755 )
197+ shutil .copytree ('scripts' , os .path .join (destination_path , 'scripts' ))
198+ shutil .copyfile ('Dockerfile' , os .path .join (destination_path , 'Dockerfile' ))
199+ return project_path
200+
201+ def prepare_mda (source_path , destination_path , artifacts_repository = None ):
202+ destination_path = prepare_destination (destination_path )
203+ mpk_file = find_default_file (source_path , '.mpk' )
122204 extracted_dir = None
123205 if mpk_file is not None :
124206 extracted_dir = extract_zip (mpk_file )
125- source_dir = extracted_dir .name
126- mpr_file = find_default_file (source_dir , '.mpr' )
207+ source_path = extracted_dir .name
208+ mpr_file = find_default_file (source_path , '.mpr' )
127209 if mpr_file is not None :
128- return build_mpr (source_dir , mpr_file )
129- mda_file = find_default_file (source_dir , '.mda' )
210+ source_path = os .path .abspath (os .path .join (mpr_file , os .pardir ))
211+ return build_mpr (source_path , mpr_file , destination_path , artifacts_repository )
212+ mda_file = find_default_file (source_path , '.mda' )
130213 if mda_file is not None :
131- extracted_dir = extract_zip (mda_file )
132- source_dir = extracted_dir .name
133- extracted_mda_file = get_metadata_value (source_dir )
134- # TODO: pre-download MxRuntime & place into CF Buildpack's cache dir
214+ with zipfile .ZipFile (mda_file ) as zip_file :
215+ zip_file .extractall (project_path )
216+ extracted_mda_file = get_metadata_value (destination_path )
135217 if extracted_mda_file is not None :
136- return source_dir
218+ return destination_path
137219 else :
138- raise Exception ('No supported files found in source dir ' )
220+ raise Exception ('No supported files found in source path ' )
139221
140222def build_image (mda_dir ):
141223 # TODO: build the full image, or just copy MDA into destination?
@@ -144,15 +226,22 @@ def build_image(mda_dir):
144226 mx_version = mda_metadata ['RuntimeVersion' ]
145227 java_version = mda_metadata .get ('JavaVersion' , 11 )
146228 print (mda_metadata ['RuntimeVersion' ])
229+ print (mda_metadata ['JavaVersion' ])
147230
148231if __name__ == '__main__' :
149- parser = argparse .ArgumentParser (description = 'Build a Docker image of a Mendix app' )
150- parser .add_argument ('source_dir' , metavar = 'source_dir' , type = pathlib .Path , help = 'Path to source Mendix app (MDA file, MPK file, MPR directory or extracted MDA directory)' )
151-
152- # TODO: allow to specify Podman args and replace URLs
232+ parser = argparse .ArgumentParser (description = 'Build a Mendix app' )
233+ parser .add_argument ('--source' , metavar = 'source' , required = True , nargs = '?' , type = pathlib .Path , help = 'Path to source Mendix app (MDA file, MPK file, MPR directory or extracted MDA directory)' )
234+ parser .add_argument ('--destination' , metavar = 'destination' , required = True , nargs = '?' , type = pathlib .Path , help = 'Destination for MDA' )
235+ parser .add_argument ('--artifacts-repository' , required = False , nargs = '?' , metavar = 'artifacts_repository' , type = str , help = 'Repository to use for caching build images' )
236+ parser .add_argument ('action' , metavar = 'action' , choices = ['build-mda' ], help = 'Action to perform' )
153237
154238 args = parser .parse_args ()
155239
156- mda_dir = prepare_mda (args .source_dir )
157- build_image (mda_dir )
240+ atexit .register (stop_processes )
241+ try :
242+ prepare_mda (args .source , args .destination , args .artifacts_repository )
243+ except KeyboardInterrupt :
244+ stop_processes ()
245+ raise
246+ # build_image(args.destination)
158247
0 commit comments