main.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project.
  3. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. import os
  8. import argparse
  9. import copy
  10. import time
  11. import tkinter as tk
  12. from tkinter import messagebox
  13. from tkinter import filedialog
  14. from config_data import ConfigData
  15. from keystore_settings import KeystoreSettings
  16. import discovery
  17. from wait_dialog import WaitDialog
  18. from keystore_generator import KeystoreGenerator
  19. from project_generator import ProjectGenerator
  20. DEFAULT_APG_CONFIG_FILE="apg_config.json"
  21. class TkApp(tk.Tk):
  22. """
  23. This is the main UI of the Android Project Generator, known as APG for short.
  24. """
  25. def __init__(self, config: ConfigData, config_file_path: str = ""):
  26. super().__init__()
  27. self.title("Android Project Generator")
  28. # Display the main window wherever the mouse is located.
  29. x, y = self.winfo_pointerx(), self.winfo_pointery()
  30. self.geometry(f"+{x}+{y}")
  31. # Set the padding for all label frames
  32. self._frame_pad_x = 4
  33. self._frame_pad_y = 4
  34. # Set the main widget's column 1 as the target expand column
  35. self.columnconfigure(0, weight=1)
  36. self._config = config
  37. self._config_file_path_var = tk.StringVar()
  38. self._config_file_path_var.set(config_file_path)
  39. self._init_load_save_ui()
  40. self._init_keystore_settings_ui()
  41. self._init_sdk_settings_ui()
  42. self._init_additional_build_settings_ui()
  43. # Add the project generation button.
  44. btn = tk.Button(self, text="Generate Project", command=self.on_generate_project_button)
  45. btn.grid()
  46. self._init_report_ui()
  47. def _init_load_save_ui(self):
  48. apg_settings_frame = tk.LabelFrame(self, text="Android Project Generator Settings")
  49. apg_settings_frame.columnconfigure(0, weight=0)
  50. apg_settings_frame.columnconfigure(1, weight=1)
  51. apg_settings_frame.grid(padx=self._frame_pad_x, pady=self._frame_pad_y, ipadx=2, ipady=2, sticky=tk.EW)
  52. self._config_file_path_var, _, row_number = self._add_label_entry(apg_settings_frame,
  53. "Config Path",
  54. self._config_file_path_var.get(),
  55. entry_colspan=1,
  56. label_width=28,
  57. entry_read_only=True)
  58. btn = tk.Button(apg_settings_frame, text="Load", command=self.on_load_settings_button)
  59. btn.grid(row=row_number, column=2, padx=2, sticky=tk.E)
  60. btn = tk.Button(apg_settings_frame, text="Save", command=self.on_save_settings_button)
  61. btn.grid(row=row_number, column=3, padx=2, sticky=tk.E)
  62. def _init_keystore_settings_ui(self):
  63. # Create a button widget with an event handler.
  64. keystore_frame = tk.LabelFrame(self, text="Keystore Settings")
  65. keystore_frame.columnconfigure(0, weight=1)
  66. keystore_frame.grid(padx=self._frame_pad_x, pady=self._frame_pad_y, sticky=tk.EW)
  67. # Let's add the fields that make the Distinguished Name.
  68. self._init_keystore_distinguished_name_ui(keystore_frame, 0)
  69. # Now let's add the rest of the keystore fields.
  70. keystore_details_frame = tk.LabelFrame(keystore_frame)
  71. keystore_details_frame.columnconfigure(0, weight=0)
  72. keystore_details_frame.columnconfigure(1, weight=1)
  73. keystore_details_frame.grid(padx=self._frame_pad_x, pady=self._frame_pad_y,row=1, column=0, sticky=tk.EW)
  74. ks_data = self._config.keystore_settings
  75. self._keystore_validity_days_var = self._add_label_entry(keystore_details_frame, "Validity Days", ks_data.validity_days, entry_colspan=3, label_width=28)[0]
  76. self._keystore_key_size_var = self._add_label_entry(keystore_details_frame, "Key Size", ks_data.key_size, entry_colspan=3)[0]
  77. self._keystore_app_key_alias_var = self._add_label_entry(keystore_details_frame, "App Key Alias", ks_data.key_alias, entry_colspan=3)[0]
  78. self._keystore_app_key_password_var = self._add_label_entry(keystore_details_frame, "App Key Password", ks_data.key_password, entry_colspan=3)[0]
  79. self._keystore_keystore_password_var = self._add_label_entry(keystore_details_frame, "Keystore Password", ks_data.keystore_password, entry_colspan=3)[0]
  80. self._keystore_file_var, _, row_number = self._add_label_entry(keystore_details_frame, "Keystore File", ks_data.keystore_file)
  81. btn = tk.Button(keystore_details_frame, text="...", command=self.on_select_keystore_file_button)
  82. btn.grid(row=row_number, column=3)
  83. btn = tk.Button(keystore_frame, text="Create Keystore", command=self.on_create_keystore_button)
  84. btn.grid()
  85. def _init_keystore_distinguished_name_ui(self, parent_frame: tk.Frame, row:int):
  86. dn_frame = tk.LabelFrame(parent_frame, text='Distinguished Name Settings')
  87. dn_frame.columnconfigure(0, weight=0)
  88. dn_frame.columnconfigure(1, weight=1)
  89. dn_frame.grid(padx=self._frame_pad_x, pady=self._frame_pad_y,row=row, sticky=tk.EW)
  90. ks_data = self._config.keystore_settings
  91. self._dn_country_code_var = self._add_label_entry(dn_frame, f"Country Code", ks_data.dn_country_code, label_width=28)[0]
  92. self._dn_company_var = self._add_label_entry(dn_frame, f"Company (aka Organization)", ks_data.dn_organization)[0]
  93. self._dn_organizational_unit_var = self._add_label_entry(dn_frame, f"Organizational Unit", ks_data.dn_organizational_unit)[0]
  94. self._dn_app_name_var = self._add_label_entry(dn_frame, f"App Name (aka Common Name)", ks_data.dn_common_name)[0]
  95. def _init_sdk_settings_ui(self):
  96. sdk_frame = tk.LabelFrame(self, text="Android SDK/NDK Settings")
  97. sdk_frame.columnconfigure(0, weight=0)
  98. sdk_frame.columnconfigure(1, weight=1)
  99. sdk_frame.grid(padx=self._frame_pad_x, pady=self._frame_pad_y,sticky=tk.EW)
  100. cf = self._config
  101. self._android_ndk_version_var = self._add_label_entry(sdk_frame, "NDK Version", cf.android_ndk_version, entry_colspan=3, label_width=28)[0]
  102. self._android_sdk_api_level_var = self._add_label_entry(sdk_frame, "SDK API Level", cf.android_sdk_api_level, entry_colspan=3)[0]
  103. self._android_sdk_path_var, _, row_number = self._add_label_entry(sdk_frame, "SDK Path", cf.android_sdk_path)
  104. sdk_path_btn = tk.Button(sdk_frame, text="...", command=self.on_select_sdk_path_button)
  105. sdk_path_btn.grid(row=row_number, column=2)
  106. # Add the meta quest project checkbox
  107. self._android_quest_flag_var, _, row_number = self._add_checkbox(sdk_frame, "This is a Meta Quest project", cf.is_meta_quest_project)
  108. def _init_additional_build_settings_ui(self):
  109. build_settings_frame = tk.LabelFrame(self, text="Additional Build Settings")
  110. build_settings_frame.columnconfigure(0, weight=0)
  111. build_settings_frame.columnconfigure(1, weight=1)
  112. build_settings_frame.grid(padx=self._frame_pad_x, pady=self._frame_pad_y,sticky=tk.EW)
  113. cf = self._config
  114. self._extra_cmake_args_var = self._add_label_entry(build_settings_frame, "Extra CMake Arguments", cf.extra_cmake_args, entry_colspan=3, label_width=28)[0]
  115. def _add_label_entry(self, parent_frame: tk.Frame, lbl_name: str, default_value: str = "", entry_colspan=1, label_width=None, entry_read_only=False) -> tuple[tk.StringVar, tk.Entry, int]:
  116. """
  117. Returns the tuple (string_var, entry, row_frame),
  118. where @string_var is the TK StringVar bound to the Entry widget,
  119. @entry is the Entry widget,
  120. @row The grid row number of inserted entry.
  121. """
  122. lbl = tk.Label(parent_frame, text=lbl_name, anchor=tk.W, width=label_width)
  123. lbl.grid(column=0, padx=5, pady=2, sticky=tk.W)
  124. row = lbl.grid_info().get("row")
  125. entry = tk.Entry(parent_frame, justify='right',state=tk.DISABLED if entry_read_only else tk.NORMAL)
  126. entry.grid(row=row, column=1, padx=5, pady=2, sticky=tk.EW, columnspan=entry_colspan)
  127. string_var = tk.StringVar()
  128. string_var.set(default_value)
  129. entry["textvariable"] = string_var
  130. return string_var, entry, row
  131. def _add_checkbox(self, parent_frame: tk.Frame, lbl_name: str, default_value: bool = False) -> tuple[tk.BooleanVar, tk.Checkbutton, int]:
  132. """
  133. Returns the tuple (BooleanVar, check_box, row_frame),
  134. where @BooleanVar is the TK BooleanVar bound to the CheckBox widget,
  135. @check_box is the Checkbutton widget,
  136. @row The grid row number of inserted entry.
  137. """
  138. bool_var = tk.BooleanVar()
  139. bool_var.set(default_value)
  140. # Create a Checkbutton widget and bind it to the variable.
  141. checkbutton = tk.Checkbutton(parent_frame, text=lbl_name, variable=bool_var)
  142. checkbutton.grid(sticky=tk.W)
  143. row_number = checkbutton.grid_info().get("row")
  144. return bool_var, checkbutton, row_number
  145. def _init_report_ui(self):
  146. """
  147. Instanties the scrollable text widget where this app will report
  148. all the stdout and stderr string produced by all subprocess invoked
  149. by this application.
  150. """
  151. operations_report_frame = tk.LabelFrame(self, text="Operations Report")
  152. operations_report_frame.columnconfigure(0, weight=1)
  153. operations_report_frame.rowconfigure(0, weight=1)
  154. operations_report_frame.grid(padx=self._frame_pad_x, pady=self._frame_pad_y,sticky=tk.NSEW)
  155. last_row = operations_report_frame.grid_info().get("row")
  156. self.rowconfigure(last_row, weight=1)
  157. self._report_text_widget = tk.Text(operations_report_frame, wrap=tk.WORD, borderwidth=2, relief=tk.SUNKEN)
  158. self._report_scrollbar_widget = tk.Scrollbar(operations_report_frame, orient=tk.VERTICAL, command=self._report_text_widget.yview)
  159. # Configure the Text widget and the Scrollbar widget.
  160. self._report_text_widget.configure(yscrollcommand=self._report_scrollbar_widget.set)
  161. self._report_text_widget.grid(sticky=tk.NSEW)
  162. self._report_scrollbar_widget.grid(row=0, column=1,sticky=tk.NSEW)
  163. def _get_time_now_str(self) -> str:
  164. """
  165. @returns The current local time as a formatted string.
  166. """
  167. time_secs = time.time()
  168. time_st = time.localtime(time_secs)
  169. no_millis_str = time.strftime("%H:%M:%S", time_st)
  170. fractional_secs = int(str(time_secs).split(".")[1])
  171. return f"{no_millis_str}.{fractional_secs}"
  172. def _append_log_message(self, msg: str):
  173. """
  174. Append the msg with a timestamp to the report widget, and automatically scrolls to
  175. the bottom of the report.
  176. """
  177. timestamp_str = self._get_time_now_str()
  178. self._report_text_widget.insert(tk.END, f">>{timestamp_str}>>\n{msg}\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n")
  179. self._report_text_widget.see(tk.END) #scroll to the end.
  180. def create_keystore_settings_from_widgets(self) -> KeystoreSettings:
  181. """
  182. @returns A new KeystoreSettings object, where all the values are read from the
  183. current content in the UI text/entry fields.
  184. """
  185. ks = KeystoreSettings()
  186. ks.keystore_file = self._keystore_file_var.get()
  187. ks.keystore_password = self._keystore_keystore_password_var.get()
  188. ks.key_alias = self._keystore_app_key_alias_var.get()
  189. ks.key_password = self._keystore_app_key_password_var.get()
  190. ks.key_size = self._keystore_key_size_var.get()
  191. ks.validity_days = self._keystore_validity_days_var.get()
  192. ks.dn_common_name = self._dn_app_name_var.get()
  193. ks.dn_organizational_unit = self._dn_organizational_unit_var.get()
  194. ks.dn_organization = self._dn_company_var.get()
  195. ks.dn_country_code = self._dn_country_code_var.get()
  196. return ks
  197. def create_config_data_from_widgets(self) -> ConfigData:
  198. """
  199. @returns A new ConfigData object, where all the values are read from the
  200. current content in the UI text/entry fields.
  201. """
  202. config = copy.deepcopy(self._config)
  203. config.android_sdk_path = self._android_sdk_path_var.get()
  204. config.android_ndk_version = self._android_ndk_version_var.get()
  205. config.android_sdk_api_level = self._android_sdk_api_level_var.get()
  206. config.is_meta_quest_project = self._android_quest_flag_var.get()
  207. config.extra_cmake_args = self._extra_cmake_args_var.get()
  208. config.keystore_settings = self.create_keystore_settings_from_widgets()
  209. return config
  210. def update_widgets_from_keystore_settings(self, ks: KeystoreSettings):
  211. self._keystore_file_var.set(ks.keystore_file)
  212. self._keystore_keystore_password_var.set(ks.keystore_password)
  213. self._keystore_app_key_alias_var.set(ks.key_alias)
  214. self._keystore_app_key_password_var.set(ks.key_password)
  215. self._keystore_key_size_var.set(ks.key_size)
  216. self._keystore_validity_days_var.set(ks.validity_days)
  217. self._dn_app_name_var.set(ks.dn_common_name)
  218. self._dn_organizational_unit_var.set(ks.dn_organizational_unit)
  219. self._dn_company_var.set(ks.dn_organization)
  220. self._dn_country_code_var.set(ks.dn_country_code)
  221. def update_widgets_from_config(self, config: ConfigData):
  222. self._android_sdk_path_var.set(config.android_sdk_path)
  223. self._android_ndk_version_var.set(config.android_ndk_version)
  224. self._android_sdk_api_level_var.set(config.android_sdk_api_level)
  225. self._android_quest_flag_var.set(config.is_meta_quest_project)
  226. self._extra_cmake_args_var.set(config.extra_cmake_args)
  227. self.update_widgets_from_keystore_settings(config.keystore_settings)
  228. def on_load_settings_button(self):
  229. """
  230. Invoked when the user clicks the `Load Settings` button.
  231. """
  232. suggested_file_path = self._config_file_path_var.get()
  233. if suggested_file_path == "":
  234. suggested_file_path = os.path.join(self._config.project_path, DEFAULT_APG_CONFIG_FILE)
  235. initial_dir, initial_file = os.path.split(suggested_file_path)
  236. filename = filedialog.askopenfilename(
  237. initialdir=initial_dir,
  238. initialfile=initial_file,
  239. title="Load Settings",
  240. filetypes=[("JSON files", "*.json"), ("All files", "*")],
  241. defaultextension=".json",
  242. parent=self
  243. )
  244. if (not filename) or (not os.path.isfile(filename)):
  245. messagebox.showinfo("Invalid Settings File Path", f"The path {filename} is invalid!")
  246. return
  247. if not self._config.load_from_json_file(filename):
  248. messagebox.showerror("File I/O Error", f"Failed to read settings from file:\n{filename}")
  249. return
  250. self._config_file_path_var.set(filename)
  251. messagebox.showinfo("Success!", f"Current settings were loaded from file:\n{filename}")
  252. self.update_widgets_from_config(self._config)
  253. def on_save_settings_button(self):
  254. """
  255. Invoked when the user clicks the `Save Settings` button.
  256. """
  257. configData = self.create_config_data_from_widgets()
  258. suggested_file_path = self._config_file_path_var.get()
  259. if suggested_file_path == "":
  260. suggested_file_path = os.path.join(configData.project_path, DEFAULT_APG_CONFIG_FILE)
  261. initial_dir, initial_file = os.path.split(suggested_file_path)
  262. filename = filedialog.asksaveasfilename(
  263. initialdir=initial_dir,
  264. initialfile=initial_file,
  265. title="Save Settings",
  266. filetypes=[("JSON files", "*.json"), ("All files", "*")],
  267. defaultextension=".json",
  268. parent=self
  269. )
  270. if (filename is None) or (filename == ""):
  271. return # Cancelled by user.
  272. if not configData.save_to_json_file(filename):
  273. messagebox.showerror("File I/O Error", f"Failed to save settings to file {filename}")
  274. self._config = configData
  275. self._config_file_path_var.set(filename)
  276. messagebox.showinfo("Success!", f"Current settings were saved as file:\n{filename}")
  277. def _on_user_cancel_task(self):
  278. """
  279. This is a callback invoked by self._wait_dialog when the user
  280. decides to cancel the current operation.
  281. """
  282. self._wait_dialog = None
  283. self._cancel_current_operation()
  284. self._current_operation = None # Doesn't hurt to force this to None.
  285. def _tick_operation(self):
  286. """
  287. This function will be called periodically while there's an operation running in the background.
  288. """
  289. if self._current_operation and self._current_operation.is_finished():
  290. # The operation completed. Time to close the progress dialog
  291. # and report the results.
  292. if self._wait_dialog:
  293. self._wait_dialog.close()
  294. self._wait_dialog = None
  295. self._on_current_operation_finished()
  296. return
  297. # Keep ticking, until next time if the operation is finished or the user
  298. # decides to cancel the current operation.
  299. if self._wait_dialog:
  300. self._wait_dialog.on_tick(float(self._tick_delta_ms)/1000.0)
  301. self.after(self._tick_delta_ms, self._tick_operation)
  302. def _cancel_current_operation(self):
  303. """
  304. This one is called upon user request.
  305. """
  306. self._current_operation.cancel()
  307. report_msg = self._current_operation.get_report_msg()
  308. self._append_log_message(report_msg)
  309. messagebox.showinfo("Cancelled By User", f"{self._current_operation.get_basic_description()}\nwas cancelled by user!")
  310. self._current_operation = None
  311. def _on_current_operation_finished(self):
  312. # The current operation is finished. But it could have
  313. # finished with error or success. Let the user know the outcome.
  314. report_msg = self._current_operation.get_report_msg()
  315. self._append_log_message(report_msg)
  316. if self._current_operation.is_success():
  317. messagebox.showinfo("Success", f"{self._current_operation.get_basic_description()}\ncompleted succesfully!")
  318. else:
  319. messagebox.showerror("Error", f"{self._current_operation.get_basic_description()}\ncompleted with errors!")
  320. self._current_operation = None
  321. def on_select_keystore_file_button(self):
  322. """
  323. The user clicked the "..." button next to the `Keystore File` field, with the purpose
  324. of selecting a different keystore file.
  325. """
  326. suggested_file_path = self._keystore_file_var.get()
  327. if suggested_file_path == "":
  328. # If the user input data is empty, try the cached data.
  329. suggested_file_path = self._config.keystore_settings.keystore_file
  330. if suggested_file_path == "":
  331. # If the cached data is empty, let's try a default
  332. suggested_file_path = os.path.join(self._config.project_path, "app.keystore")
  333. initial_dir, initial_file = os.path.split(suggested_file_path)
  334. filename = filedialog.asksaveasfilename(
  335. initialdir=initial_dir,
  336. initialfile=initial_file,
  337. title="Select Keystore File",
  338. filetypes=[("keystore files", "*.keystore"), ("All files", "*")],
  339. defaultextension=".json",
  340. parent=self
  341. )
  342. if (not filename) or (filename == ""):
  343. messagebox.showerror("Error", f"Invalid Keystore File Path!")
  344. return
  345. self._keystore_file_var.set(filename)
  346. self._config.keystore_settings.keystore_file = filename
  347. def on_select_sdk_path_button(self):
  348. """
  349. The user clicked the "..." button next to the `SDK Path` field, with the purpose
  350. of selecting a different Android SDK path.
  351. """
  352. configData = self.create_config_data_from_widgets()
  353. initial_dir = configData.android_sdk_path
  354. directory = filedialog.askdirectory(
  355. initialdir=initial_dir,
  356. title="Pick Android SDK Location",
  357. parent=self
  358. )
  359. if (not directory ) or (directory == "") or (not os.path.isdir(directory)):
  360. messagebox.showerror("Invalid SDK Path", f"The path {directory} is invalid!")
  361. return
  362. if not discovery.could_be_android_sdk_directory(directory):
  363. messagebox.showwarning("Warning", f"The directory:\n{directory}\nDoesn't appear to be an Android SDK directory.")
  364. configData.android_sdk_path = directory
  365. self._config = configData
  366. self.update_widgets_from_config(self._config)
  367. def on_create_keystore_button(self):
  368. """
  369. The user clicked the `Create Keystore` button. Will spawn the required tools to create the keystore.
  370. """
  371. ks = self.create_keystore_settings_from_widgets()
  372. if (not ks.keystore_file) or (ks.keystore_file == ""):
  373. messagebox.showerror("Error", f"A vaid `Keystore File` is required.")
  374. return
  375. if os.path.isfile(ks.keystore_file):
  376. result = messagebox.askyesno("Attention!", f"Do you want to replace the Keystore File:\n{ks.keystore_file}?")
  377. if not result:
  378. return
  379. else:
  380. # It's important to delete the existing keystore file, otherwise the java keytool will fail to replace it.
  381. try:
  382. os.remove(ks.keystore_file)
  383. except Exception as err:
  384. messagebox.showerror("Error", f"Failed to delete keystore file {ks.keystore_file}. Got Exception:\n{err}")
  385. return
  386. self._config.keystore_settings = ks
  387. # Start the in-progress modal dialog.
  388. def _inner_cancel_cb():
  389. self._on_user_cancel_task()
  390. self._wait_dialog = WaitDialog(self, "Creating Keystore.\nThis operation takes around 5 seconds.", _inner_cancel_cb)
  391. self._tick_delta_ms = 250
  392. self.after(self._tick_delta_ms, self._tick_operation)
  393. # Instantiate and start the job.
  394. self._current_operation = KeystoreGenerator(self._config)
  395. self._current_operation.start()
  396. def on_generate_project_button(self):
  397. """
  398. The user clicked the `Generate Project` button. Will spawn the required tools to create the android project.
  399. """
  400. configData = self.create_config_data_from_widgets()
  401. # Make sure the keystore file exist.
  402. ks = configData.keystore_settings
  403. if (not ks.keystore_file) or (ks.keystore_file == ""):
  404. messagebox.showerror("Error", f"Can not generate an android project without a valid `Keystore File`.")
  405. return
  406. if not os.path.isfile(ks.keystore_file):
  407. messagebox.showerror("Error", f"The keystore file {ks.keystore_file} doesn't exist.\nPush the `Create Keystore` button to create it.")
  408. return
  409. # Start the in-progress modal dialog.
  410. def _inner_cancel_cb():
  411. self._on_user_cancel_task()
  412. self._wait_dialog = WaitDialog(self, "Generating the android project.\nThis operation takes around 30 seconds.", _inner_cancel_cb)
  413. self._tick_delta_ms = 250
  414. self.after(self._tick_delta_ms, self._tick_operation)
  415. # Instantiate and start the job.
  416. self._config = configData
  417. self._current_operation = ProjectGenerator(configData)
  418. self._current_operation.start()
  419. # class TkApp END
  420. ######################################################
  421. if __name__ == "__main__":
  422. parser = argparse.ArgumentParser(description='Presents a UI that automates the generation of an Android Project and its keystore.')
  423. parser.add_argument('--engine', '--e', required=True,
  424. help='Path to the engine root directory.')
  425. parser.add_argument('--project', '--p', required=True,
  426. help='Path to the project root directory.')
  427. parser.add_argument('--build', '--b', required=True,
  428. help='Path to the build directory.')
  429. parser.add_argument('--third_party', '--t', required=True,
  430. help='Path to the 3rd Party root folder.')
  431. args = parser.parse_args()
  432. # Check if the project directory contains a json file with configuration data.
  433. configPath = os.path.join(args.project, DEFAULT_APG_CONFIG_FILE)
  434. config_data = ConfigData()
  435. config_data.load_from_json_file(configPath)
  436. # Discover the location of the keystore file if not defined yet.
  437. ks = config_data.keystore_settings
  438. if (not ks.keystore_file) or (ks.keystore_file == ""):
  439. ks.keystore_file = os.path.join(args.project, "app.keystore")
  440. # Discover the android sdk path if empty.
  441. if (config_data.android_sdk_path is None) or (config_data.android_sdk_path == ""):
  442. config_data.android_sdk_path = discovery.discover_android_sdk_path()
  443. config_data.engine_path = args.engine
  444. config_data.project_path = args.project
  445. config_data.build_path = args.build
  446. config_data.third_party_path = args.third_party
  447. app = TkApp(config_data, configPath)
  448. app.mainloop()