├── .gitignore ├── LICENSE ├── README.md ├── addons └── godot-package-manager │ ├── cli.gd │ ├── godot_package_manager.gd │ ├── main.gd │ ├── main.tscn │ ├── plugin.cfg │ └── plugin.gd ├── default_env.tres ├── godot.lock ├── godot.package ├── icon.png ├── icon.png.import ├── project.godot └── runner ├── dummy_plugin.gd ├── main.theme ├── runner.gd └── runner.tscn /.gitignore: -------------------------------------------------------------------------------- 1 | # System 2 | .DS_Store 3 | *.swp 4 | *.swo 5 | .vs/ 6 | .vscode/ 7 | 8 | # Rust 9 | target/ 10 | Cargo.lock 11 | **/*.rs.bk 12 | **/*.dll 13 | **/*.so 14 | 15 | # Godot 16 | .import/ 17 | export.cfg 18 | export_presets.cfg 19 | .mono/ 20 | data_*/ 21 | export/ 22 | export_templates/ 23 | 24 | **/temp 25 | addons/__gpm_deps 26 | addons/advanced-expression 27 | addons/test 28 | addons/verbal-expressions 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | License shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | Licensor shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | Legal Entity shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | control means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | You (or Your) shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | Source form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | Object form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | Work shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | Derivative Works shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | Contribution shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, submitted 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as Not a Contribution. 61 | 62 | Contributor shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a NOTICE text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an AS IS BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets [] 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same printed page as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Timothy Yuen 190 | 191 | Licensed under the Apache License, Version 2.0 (the License); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an AS IS BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot Package Manager plugin for godot 2 | 3 | [![discord](https://img.shields.io/discord/853476898071117865?label=chat&logo=discord&style=for-the-badge&logoColor=white)](https://discord.gg/6mcdWWBkrr "Chat on Discord") 4 | [![version](https://img.shields.io/badge/3.x-blue?logo=godot-engine&logoColor=white&label=godot&style=for-the-badge)](https://godotengine.org "Made for godot") 5 | 6 | > **Warning** This gpm implementation does _not_ support yaml, toml and hjson, only pure json is supported. 7 | 8 | ## Installation 9 | 10 | > **Note** read the [using packages quickstart](https://github.com/godot-package-manager#using-packages-quickstart) first. 11 | 12 | 1. Clone the repository or [grab the latest release](https://github.com/you-win/godot-package-manager/releases/latest) 13 | 2. Place the `addons/godot-package-manager` folder into your project's `addons` directory 14 | 3. Enable the plugin in Godot. A new menu will appear in the editor's bottom panel, that contains the update and purge buttons 15 | -------------------------------------------------------------------------------- /addons/godot-package-manager/cli.gd: -------------------------------------------------------------------------------- 1 | extends SceneTree 2 | 3 | #-----------------------------------------------------------------------------# 4 | # Builtin functions # 5 | #-----------------------------------------------------------------------------# 6 | 7 | 8 | func _initialize() -> void: 9 | var gpm = preload("./godot_package_manager.gd").new() 10 | gpm.connect("operation_finished", self, "_on_finished") 11 | gpm.connect("message_logged", self, "_on_message_logged") 12 | gpm.connect("operation_started", self, "_on_operation_started") 13 | 14 | var call_func := "" 15 | 16 | var args := OS.get_cmdline_args() 17 | for arg in args: 18 | arg = (arg as String).lstrip("-").trim_prefix("package-") 19 | 20 | if arg in ["update", "purge"]: 21 | call_func = arg 22 | break 23 | 24 | if call_func.empty(): 25 | printerr("No action specified") 26 | quit(1) 27 | return 28 | 29 | gpm.call(call_func) 30 | 31 | 32 | #-----------------------------------------------------------------------------# 33 | # Connections # 34 | #-----------------------------------------------------------------------------# 35 | 36 | func _on_operation_started(op_name: String, num_packages: int) -> void: 37 | print("Operation started: %s (%d packages)" % [op_name, num_packages]) 38 | 39 | 40 | func _on_finished() -> void: 41 | print("Finished!") 42 | quit(0) 43 | 44 | 45 | func _on_message_logged(message: String) -> void: 46 | print(message) 47 | 48 | #-----------------------------------------------------------------------------# 49 | # Private functions # 50 | #-----------------------------------------------------------------------------# 51 | 52 | #-----------------------------------------------------------------------------# 53 | # Public functions # 54 | #-----------------------------------------------------------------------------# 55 | -------------------------------------------------------------------------------- /addons/godot-package-manager/godot_package_manager.gd: -------------------------------------------------------------------------------- 1 | extends Reference 2 | 3 | #region NPM utils 4 | 5 | class NPMUtils: 6 | # Converts a package.json to a godot.package format 7 | # 8 | # @result: Dictionary 9 | static func npm_to_godot(npm: Dictionary) -> Dictionary: 10 | var new_d := {PackageKeys.PACKAGES: {}} 11 | var npm_deps: Dictionary = npm.get(NpmPackageKeys.PACKAGES, {}) 12 | for pkg in npm_deps.keys(): 13 | new_d[PackageKeys.PACKAGES][pkg] = npm_deps[pkg] 14 | return new_d 15 | 16 | static func get_package_file(package_name: String, ver: String) -> PackageResult: 17 | var res: PackageResult = yield( 18 | NetUtils.send_get_request("https://cdn.jsdelivr.net", "npm/%s@%s/package.json" % [package_name, ver]), 19 | "completed" 20 | ) 21 | if res.is_err(): 22 | return res 23 | res = Utils.json_to_dict(res.unwrap().get_string_from_utf8()) 24 | if res.is_err(): 25 | return res 26 | return PackageResult.ok(res.unwrap()) 27 | 28 | static func get_package_file_godot(package_name: String, ver: String) -> PackageResult: 29 | var res: PackageResult = yield(get_package_file(package_name, ver), "completed") 30 | if res.is_err(): 31 | return res 32 | return PackageResult.ok(npm_to_godot(res.unwrap())) 33 | 34 | 35 | #endregion 36 | 37 | 38 | #region misc utils 39 | class Utils: 40 | # flattens a dictionary and returns a array of the values 41 | static func flatten(d: Dictionary) -> Array: 42 | var a := [] 43 | _flatten_inner(d, a) 44 | return a 45 | 46 | 47 | static func _flatten_inner(d: Dictionary, a: Array) -> void: 48 | for v in d.values(): 49 | if v is Dictionary: 50 | _flatten_inner(v, a) 51 | continue 52 | a.append(v) 53 | 54 | static func compile_regex(src: String) -> RegEx: 55 | var r := RegEx.new() 56 | r.compile(src) 57 | return r 58 | 59 | static func remove_start(string: String, remove: String) -> String: 60 | if string.begins_with(remove): 61 | return string.substr(len(remove)) 62 | return string 63 | 64 | static func json_to_dict(json_string: String) -> PackageResult: 65 | var parse_result := JSON.parse(json_string) 66 | if parse_result.error != OK: 67 | return PackageResult.err(Error.Code.INVALID_JSON) 68 | 69 | if typeof(parse_result.result) != TYPE_DICTIONARY: 70 | return PackageResult.err(Error.Code.UNEXPECTED_JSON_FORMAT) 71 | 72 | return PackageResult.ok(parse_result.result) 73 | 74 | static func insert(t: String, insertion: String, begin: int, end: int) -> String: 75 | return t.left(begin) + insertion + t.right(end) 76 | 77 | #endregion 78 | 79 | #region Network utils 80 | 81 | class NetUtils: 82 | ## Send a GET request to a given host/path 83 | ## 84 | ## @param: host: String - The host to connect to 85 | ## @param: path: String - The host path 86 | ## 87 | ## @return: PackageResult[PoolByteArray] - The response body 88 | static func send_get_request(host: String, path: String) -> PackageResult: 89 | var http := HTTPClient.new() 90 | 91 | var err := http.connect_to_host(host, 443, true) 92 | if err != OK: 93 | return PackageResult.err(Error.Code.CONNECT_TO_HOST_FAILURE, host) 94 | 95 | while http.get_status() in CONNECTING_STATUS: 96 | http.poll() 97 | yield(Engine.get_main_loop(), "idle_frame") 98 | 99 | if http.get_status() != HTTPClient.STATUS_CONNECTED: 100 | return PackageResult.err(Error.Code.UNABLE_TO_CONNECT_TO_HOST, host) 101 | 102 | err = http.request(HTTPClient.METHOD_GET, "/%s" % path, HEADERS) 103 | if err != OK: 104 | return PackageResult.err(Error.Code.GET_REQUEST_FAILURE, path) 105 | 106 | while http.get_status() == HTTPClient.STATUS_REQUESTING: 107 | http.poll() 108 | yield(Engine.get_main_loop(), "idle_frame") 109 | 110 | if not http.get_status() in SUCCESS_STATUS: 111 | return PackageResult.err(Error.Code.UNSUCCESSFUL_REQUEST, path) 112 | 113 | if http.get_response_code() != 200: 114 | return PackageResult.err(Error.Code.UNEXPECTED_STATUS_CODE, "%s - %d" % [path, http.get_response_code()]) 115 | 116 | var body := PoolByteArray() 117 | 118 | while http.get_status() == HTTPClient.STATUS_BODY: 119 | http.poll() 120 | 121 | var chunk := http.read_response_body_chunk() 122 | if chunk.size() == 0: 123 | yield(Engine.get_main_loop(), "idle_frame") 124 | else: 125 | body.append_array(chunk) 126 | 127 | return PackageResult.ok(body) 128 | 129 | 130 | #endregion 131 | 132 | #region Directory utils 133 | 134 | 135 | class DirUtils: 136 | ## Recursively finds all files in a directory. Nested directories are represented by further dicts 137 | ## 138 | ## @param: original_path: String - The absolute, root path of the directory. Used to strip out the full path 139 | ## @param: path: String - The current, absoulute search path 140 | ## 141 | ## @return: Dictionary - The files + directories in the current `path` 142 | ## 143 | ## @example: original_path: /my/path/to/ 144 | ## { 145 | ## "nested": { 146 | ## "hello.gd": "/my/path/to/nested/hello.gd" 147 | ## }, 148 | ## "file.gd": "/my/path/to/file.gd" 149 | ### } 150 | static func _get_files_recursive_inner(original_path: String, path: String) -> Dictionary: 151 | var r := {} 152 | 153 | var dir := Directory.new() 154 | if dir.open(path) != OK: 155 | printerr("Failed to open directory path: %s" % path) 156 | return r 157 | 158 | dir.list_dir_begin(true, false) 159 | 160 | var file_name := dir.get_next() 161 | 162 | while file_name != "": 163 | var full_path := dir.get_current_dir().plus_file(file_name) 164 | if dir.current_is_dir(): 165 | r[path.replace(original_path, "").plus_file(file_name)] = _get_files_recursive_inner( 166 | original_path, full_path 167 | ) 168 | else: 169 | r[file_name] = full_path 170 | 171 | file_name = dir.get_next() 172 | 173 | return r 174 | 175 | ## Wrapper for _get_files_recursive(..., ...) omitting the `original_path` arg. 176 | ## 177 | ## @param: path: String - The path to search 178 | ## 179 | ## @return: Dictionary - A recursively `Dictionary` of all files found at `path` 180 | static func get_files_recursive(path: String) -> Dictionary: 181 | return _get_files_recursive_inner(path, path) 182 | 183 | ## Removes a directory recursively 184 | ## 185 | ## @param: path: String - The path to remove 186 | ## @param: delete_base_dir: bool - Whether to remove the root directory at path as well 187 | ## @param: file_dict: Dictionary - The result of `_get_files_recursive` if available 188 | ## 189 | ## @return: int - The error code 190 | static func remove_dir_recursive(path: String, delete_base_dir: bool = true, file_dict: Dictionary = {}) -> int: 191 | var files := DirUtils.get_files_recursive(path) if file_dict.empty() else file_dict 192 | 193 | for key in files.keys(): 194 | var file_path: String = path.plus_file(key) 195 | var val = files[key] 196 | 197 | if val is Dictionary: 198 | if DirUtils.remove_dir_recursive(file_path, false) != OK: 199 | printerr("Unable to remove_dir_recursive") 200 | return ERR_BUG 201 | 202 | if OS.move_to_trash(ProjectSettings.globalize_path(file_path)) != OK: 203 | printerr("Unable to remove file at path: %s" % file_path) 204 | return ERR_BUG 205 | 206 | if delete_base_dir and OS.move_to_trash(ProjectSettings.globalize_path(path)) != OK: 207 | printerr("Unable to remove file at path: %s" % path) 208 | return ERR_BUG 209 | 210 | return OK 211 | 212 | 213 | #endregion 214 | 215 | #region File utils 216 | 217 | 218 | class FileUtils: 219 | static func save_string(string: String, path: String) -> PackageResult: 220 | var file := File.new() 221 | if file.open(path, File.WRITE) != OK: 222 | return PackageResult.err(Error.Code.FILE_OPEN_FAILURE, path) 223 | 224 | file.store_string(string) 225 | file.close() 226 | return PackageResult.ok() 227 | 228 | static func save_data(data: PoolByteArray, path: String) -> PackageResult: 229 | var file := File.new() 230 | if file.open(path, File.WRITE) != OK: 231 | return PackageResult.err(Error.Code.FILE_OPEN_FAILURE, path) 232 | 233 | file.store_buffer(data) 234 | 235 | file.close() 236 | 237 | return PackageResult.ok() 238 | 239 | static func read_file_to_string(path: String) -> PackageResult: 240 | var file := File.new() 241 | if file.open(path, File.READ) != OK: 242 | return PackageResult.err(Error.Code.FILE_OPEN_FAILURE, path) 243 | 244 | var t := PackageResult.ok(file.get_as_text()) 245 | return t 246 | 247 | static func absolute_to_relative(path: String, cwd: String, remove_res := true) -> String: 248 | if remove_res: 249 | path = Utils.remove_start(path, "res://") 250 | cwd = Utils.remove_start(cwd, "res://") 251 | var common := cwd 252 | var result := "" 253 | while Utils.remove_start(path, common) == path: 254 | common = common.get_base_dir() 255 | 256 | if !result: 257 | result = ".." 258 | else: 259 | result = "../" + result 260 | 261 | if common == "/": 262 | result += "/" 263 | 264 | var uncommon := Utils.remove_start(path, common) 265 | if result and uncommon: 266 | result += uncommon 267 | elif uncommon: 268 | result = uncommon.substr(1) 269 | return result 270 | 271 | ## Emulates `tar xzf --strip-components=1 -C ` 272 | ## 273 | ## @param: file_path: String - The relative file path to a tar file 274 | ## @param: output_path: String - The file path to extract to 275 | ## 276 | ## @return: PackageResult[] - The result of the operation 277 | static func xzf(file_path: String, output_path: String) -> PackageResult: 278 | var output := [] 279 | var exit_code := OS.execute( 280 | "tar", 281 | [ 282 | "xzf", 283 | ProjectSettings.globalize_path(file_path), 284 | "--strip-components=1", 285 | "-C", 286 | ProjectSettings.globalize_path(output_path) 287 | ], 288 | true, 289 | output 290 | ) 291 | 292 | # `tar xzf` should not produce any output 293 | if exit_code != 0: 294 | printerr(output) 295 | return PackageResult.err(Error.Code.GENERIC, "Tar failed (%s)" % exit_code) 296 | 297 | return PackageResult.ok() 298 | 299 | 300 | #endregion 301 | 302 | #region Error handling 303 | 304 | const DEFAULT_ERROR := "Default error" 305 | 306 | 307 | class Error: 308 | enum Code { 309 | NONE = 0, 310 | GENERIC, 311 | 312 | #region JSON handling 313 | 314 | INVALID_JSON, 315 | UNEXPECTED_JSON_FORMAT, 316 | 317 | #endregion 318 | 319 | #region Http requests 320 | 321 | INITIATE_CONNECT_TO_HOST_FAILURE, 322 | UNABLE_TO_CONNECT_TO_HOST, 323 | 324 | UNSUCCESSFUL_REQUEST, 325 | MISSING_RESPONSE, 326 | UNEXPECTED_STATUS_CODE, 327 | 328 | GET_REQUEST_FAILURE, 329 | 330 | UNZIP_FAILURE, 331 | 332 | #endregion 333 | 334 | #region Config 335 | 336 | FILE_OPEN_FAILURE, 337 | PARSE_FAILURE, 338 | UNEXPECTED_DATA, 339 | 340 | NO_PACKAGES, 341 | PROCESS_PACKAGES_FAILURE 342 | 343 | REMOVE_PACKAGE_DIR_FAILURE 344 | CREATE_PACKAGE_DIR_FAILURE 345 | 346 | #endregion 347 | 348 | #region AdvancedExpression 349 | 350 | MISSING_SCRIPT, 351 | BAD_SCRIPT_TYPE, 352 | SCRIPT_COMPILE_FAILURE, 353 | 354 | #endregion 355 | } 356 | 357 | var _error: int 358 | var _description: String 359 | 360 | func _init(error: int, description: String = "") -> void: 361 | _error = error 362 | _description = description 363 | 364 | func _to_string() -> String: 365 | return "Code: %d\nName: %s\nDescription: %s" % [_error, error_name(), _description] 366 | 367 | func error_code() -> int: 368 | return _error 369 | 370 | func error_name() -> int: 371 | return Code.keys()[_error] 372 | 373 | func error_description() -> String: 374 | return _description 375 | 376 | 377 | class PackageResult: 378 | var _value 379 | var _error: Error 380 | 381 | func _init(v) -> void: 382 | if not v is Error: 383 | _value = v 384 | else: 385 | _error = v 386 | 387 | func _to_string() -> String: 388 | if is_err(): 389 | return "ERR: %s" % str(_error) 390 | else: 391 | return "OK: %s" % str(_value) 392 | 393 | func is_ok() -> bool: 394 | return not is_err() 395 | 396 | func is_err() -> bool: 397 | return _error != null 398 | 399 | func unwrap(): 400 | return _value 401 | 402 | func unwrap_err() -> Error: 403 | return _error 404 | 405 | func expect(text: String): 406 | if is_err(): 407 | printerr(text) 408 | return null 409 | return _value 410 | 411 | func or_else(val): 412 | return _value if is_ok() else val 413 | 414 | static func ok(v = null) -> PackageResult: 415 | var res = PackageResult.new(OK) 416 | return PackageResult.new(v if v != null else OK) 417 | 418 | static func err(error_code: int = 1, description: String = "") -> PackageResult: 419 | return PackageResult.new(Error.new(error_code, description)) 420 | 421 | 422 | #endregion 423 | 424 | 425 | class FailedPackages: 426 | var failed_package_log := [] # Log of failed packages and reasons 427 | var failed_packages := [] # Array of failed package names only 428 | 429 | func add(package_name: String, reason: String) -> void: 430 | failed_package_log.append("%s - %s" % [package_name, reason]) 431 | failed_packages.append(package_name) 432 | 433 | func get_logs() -> String: 434 | failed_package_log.invert() 435 | return PoolStringArray(failed_package_log).join("\n") 436 | 437 | func has_logs() -> bool: 438 | return not failed_package_log.empty() 439 | 440 | func get_failed_packages() -> Array: 441 | return failed_packages.duplicate() 442 | 443 | 444 | class Hooks: 445 | var _hooks := {} # Hook name: String -> AdvancedExpression 446 | 447 | func add(hook_name: String, advanced_expression: AdvancedExpression) -> void: 448 | _hooks[hook_name] = advanced_expression 449 | 450 | ## Runs the given hook if it exists. Requires the containing GPM to be passed 451 | ## since all scripts assume they have access to a `gpm` variable 452 | ## 453 | ## @param: gpm: Object - The containing GPM. Must be a valid `Object` 454 | ## @param: hook_name: String - The name of the hook to run 455 | ## 456 | ## @return: Variant - The return value, if any. Will return `null` if the hook is not found 457 | func run(gpm: Object, hook_name: String): 458 | return _hooks[hook_name].execute([gpm]) if _hooks.has(hook_name) else null 459 | 460 | 461 | #region Package classes & cfg handling 462 | class Package: 463 | var unscoped_name: String setget , _get_unscoped_name 464 | var name: String = "" 465 | var download_dir: String = "" setget , _get_download_dir 466 | var integrity: String = "" 467 | var version: String = "" 468 | var installed: bool = false setget , _get_is_installed 469 | var indirect: bool = false 470 | var dependencies := PackageList.new() 471 | 472 | var required_when: AdvancedExpression = null 473 | var optional_when: AdvancedExpression = null 474 | 475 | var npm_manifest: Dictionary 476 | 477 | var gpm: Object = null 478 | 479 | func _get_unscoped_name() -> String: 480 | return name.get_file() 481 | 482 | func _get_download_dir() -> String: 483 | return ( 484 | (DEPENDENCIES_DIR_FORMAT % [self.unscoped_name, version]) 485 | if indirect 486 | else (ADDONS_DIR_FORMAT % self.unscoped_name) 487 | ) 488 | 489 | func _get_is_installed() -> bool: 490 | return Directory.new().dir_exists(self.download_dir) 491 | 492 | func _init(name: String, version: String, gpm: Object, indirect: bool = false) -> void: 493 | self.name = name 494 | self.version = version 495 | self.gpm = gpm 496 | self.indirect = indirect 497 | 498 | func _to_string() -> String: 499 | return "[Package:%s@%s]" % [name, version] 500 | 501 | #region utils (lowlevel stuff) 502 | 503 | func get_manifest() -> PackageResult: 504 | npm_manifest = {} 505 | var res: PackageResult = yield(NetUtils.send_get_request(REGISTRY, "%s/%s" % [name, version]), "completed") 506 | if res.is_err(): 507 | return res 508 | 509 | var body: String = res.unwrap().get_string_from_utf8() 510 | var parse_res := Utils.json_to_dict(body) 511 | if parse_res.is_err(): 512 | return parse_res 513 | 514 | var tmp_manifest: Dictionary = parse_res.unwrap() 515 | 516 | if not NpmManifestKeys.DIST in tmp_manifest or not tmp_manifest[NpmManifestKeys.DIST].has(NpmManifestKeys.TARBALL): 517 | return PackageResult.err(Error.Code.UNEXPECTED_DATA, "%s - NPM manifest missing required fields" % name) 518 | 519 | npm_manifest = tmp_manifest 520 | integrity = npm_manifest["dist"]["integrity"] 521 | return PackageResult.ok(npm_manifest) 522 | 523 | func get_tarball(download_location: String) -> PackageResult: 524 | var res: PackageResult = yield( 525 | NetUtils.send_get_request( 526 | REGISTRY, npm_manifest[NpmManifestKeys.DIST][NpmManifestKeys.TARBALL].replace(REGISTRY, "") 527 | ), 528 | "completed" 529 | ) 530 | if not res or res.is_err(): 531 | return res 532 | 533 | var downloaded_file: PoolByteArray = res.unwrap() 534 | res = FileUtils.save_data(downloaded_file, download_location) 535 | if not res or res.is_err(): 536 | return res 537 | return PackageResult.ok() 538 | 539 | 540 | func _modify_script_loads(t: String, cwd: String) -> PackageResult: 541 | var script_load_r := Utils.compile_regex('(pre)?load\\(\\"([^)]+)\\"\\)') 542 | var offset := 0 543 | for m in script_load_r.search_all(t): 544 | # m.strings[(the entire match), (the pre part), (group1)] 545 | var is_preload = m.strings[1] == "pre" 546 | var res := _modify_load(m.strings[2], is_preload, cwd, ("preload" if is_preload else "load") + '("%s")') 547 | if res.is_err(): 548 | return res 549 | var p: String = res.unwrap() 550 | if p.empty(): continue 551 | var t_l := len(t) 552 | t = Utils.insert(t, p, m.get_start() + offset, m.get_end() + offset) 553 | offset += len(t) - t_l 554 | return PackageResult.ok(t) 555 | 556 | 557 | func _modify_text_resource_loads(t: String, cwd: String) -> PackageResult: 558 | var scene_load_r := Utils.compile_regex('\\[ext_resource path="([^"]+)"') 559 | var offset := 0 560 | for m in scene_load_r.search_all(t): 561 | var res := _modify_load(m.strings[1], false, cwd, '[ext_resource path="%s"') 562 | if res.is_err(): 563 | return res 564 | var p: String = res.unwrap() 565 | if p.empty(): continue 566 | var t_l := len(t) 567 | t = Utils.insert(t, p, m.get_start() + offset, m.get_end() + offset) 568 | offset += len(t) - t_l 569 | return PackageResult.ok(t) 570 | 571 | 572 | func _modify_load(path: String, is_preload: bool, cwd: String, f_str: String) -> PackageResult: 573 | var F := File.new() 574 | if F.file_exists(path) or F.file_exists(cwd.plus_file(path)): 575 | var is_rel := path.begins_with(".") 576 | if not is_rel: 577 | var rel := FileUtils.absolute_to_relative(path, cwd) 578 | if len(path) > len(rel): 579 | return PackageResult.ok(f_str % rel) 580 | return PackageResult.ok("") 581 | path = Utils.remove_start(path, "res://addons") 582 | var split := path.split("/") 583 | var wanted_addon := split[1] 584 | var wanted_file := PoolStringArray(Array(split).slice(2, len(split) - 1)).join("/") 585 | var noscope_cfg: Dictionary = {} 586 | for pkg in dependencies: 587 | noscope_cfg[pkg.unscoped_name] = pkg.download_dir 588 | if wanted_addon in noscope_cfg: 589 | var wanted_f: String = noscope_cfg[wanted_addon].plus_file(wanted_file) 590 | if is_preload: 591 | var rel := FileUtils.absolute_to_relative(wanted_f, cwd) 592 | if len(wanted_f) > len(rel): 593 | PackageResult.ok(f_str % rel) 594 | return PackageResult.ok(f_str % wanted_f) 595 | return PackageResult.err(Error.Code.GENERIC, "Could not find path for %s" % path) 596 | 597 | #endregion 598 | 599 | # scan for load and preload funcs and have their paths modified 600 | # - preload will be modified to use a relative path, if the relative path is shorter than the absolute path.s 601 | # - load will be modified to use an absolute path (they are not preprocessored, must be absolute) 602 | func modify() -> PackageResult: 603 | if not self.installed: 604 | return PackageResult.err(Error.Code.GENERIC, "Not installed") 605 | 606 | for f in Utils.flatten(DirUtils.get_files_recursive(self.download_dir)): 607 | if ResourceLoader.exists(f): 608 | var ext: String = f.split(".")[-1] 609 | var modify_func: FuncRef 610 | 611 | if ext in ["gd", "gdscript"]: 612 | modify_func = funcref(self, "_modify_script_loads") 613 | elif ext in ["tscn", "tres"]: 614 | modify_func = funcref(self, "_modify_text_resource_loads") 615 | else: 616 | continue 617 | var res := FileUtils.read_file_to_string(f) 618 | 619 | if res.is_err(): 620 | return res 621 | var file_contents: String = res.unwrap() 622 | res = modify_func.call_func(file_contents, f.get_base_dir()) 623 | 624 | if res.is_err(): 625 | return res 626 | var new_file_contents: String = res.unwrap() 627 | if file_contents != new_file_contents: 628 | res = FileUtils.save_string(new_file_contents, f) 629 | if res.is_err(): 630 | return res 631 | return PackageResult.ok() 632 | 633 | func should_download() -> bool: 634 | if required_when || optional_when: 635 | for n in [[required_when, 0], [optional_when, 1]]: 636 | var execute_value = n[0].execute([gpm]) 637 | if typeof(execute_value) == TYPE_BOOL: 638 | match n[1]: 639 | 0: # reqwhen 640 | return execute_value != false 641 | 1: # optwhen 642 | return execute_value != true 643 | return true 644 | 645 | func download() -> PackageResult: 646 | gpm.emit_signal("operation_checkpoint_reached", name) 647 | yield(Engine.get_main_loop(), "idle_frame") # return a GDScriptFunctionState 648 | if not should_download(): 649 | gpm.emit_signal("message_logged", "Skipping package %s because of condition" % name) 650 | return PackageResult.ok() 651 | 652 | var dir := Directory.new() 653 | if dir.dir_exists(self.download_dir): 654 | if DirUtils.remove_dir_recursive(self.download_dir) != OK: 655 | return PackageResult.err(Error.Code.REMOVE_PACKAGE_DIR_FAILURE) 656 | 657 | var download_location: String = ( 658 | ADDONS_DIR_FORMAT 659 | % npm_manifest[NpmManifestKeys.DIST][NpmManifestKeys.TARBALL].get_file() 660 | ) 661 | var res: PackageResult = yield(get_tarball(download_location), "completed") 662 | if res.is_err(): 663 | return res 664 | 665 | if not dir.dir_exists(self.download_dir) and dir.make_dir_recursive(self.download_dir) != OK: 666 | return PackageResult.err(Error.Code.CREATE_PACKAGE_DIR_FAILURE) 667 | res = FileUtils.xzf(download_location, self.download_dir) 668 | if not res or res.is_err(): 669 | return PackageResult.err(Error.Code.UNZIP_FAILURE) 670 | 671 | if dir.remove(download_location) != OK: 672 | return PackageResult.err(Error.Code.FILE_OPEN_FAILURE) 673 | return modify() 674 | 675 | func depend(on: Package) -> void: 676 | dependencies.append(on) 677 | 678 | 679 | class PackageList: 680 | var _packages := [] 681 | var _iter_current: int 682 | 683 | func add(package: Package) -> PackageList: 684 | _packages.append(package) 685 | return self 686 | 687 | func append(package: Package) -> void: 688 | _packages.append(package) 689 | 690 | func _should_continue() -> bool: 691 | return len(_packages) > _iter_current 692 | 693 | func _iter_init(arg) -> bool: 694 | _iter_current = 0 695 | return _should_continue() 696 | 697 | func _iter_next(arg) -> bool: 698 | _iter_current += 1 699 | return _should_continue() 700 | 701 | func _iter_get(arg) -> Package: 702 | return _packages[_iter_current] 703 | 704 | func size() -> int: 705 | return _packages.size() 706 | 707 | func _to_string() -> String: 708 | return "PackageList" + str(_packages) 709 | 710 | 711 | class WantedPackages: 712 | extends PackageList 713 | var hooks: Hooks = null 714 | var gpm: Object = null 715 | 716 | func _init(gpm: Object) -> void: 717 | self.gpm = gpm 718 | 719 | func _add(p: Package, cfg: Dictionary) -> PackageResult: 720 | append(p) 721 | #region scripts 722 | if p.indirect == false: 723 | var data = cfg[PackageKeys.PACKAGES][p.name] 724 | if typeof(data) == TYPE_DICTIONARY: 725 | for n in [PackageKeys.REQUIRED_WHEN, PackageKeys.OPTIONAL_WHEN]: 726 | if data.has(n): 727 | var res: PackageResult = ScriptUtils.dict_to_script(data[n], cfg) 728 | if res.is_err(): 729 | return res 730 | match n: 731 | PackageKeys.REQUIRED_WHEN: 732 | p.required_when = res.unwrap() 733 | PackageKeys.OPTIONAL_WHEN: 734 | p.optional_when = res.unwrap() 735 | #endregion 736 | var res: PackageResult = yield(p.get_manifest(), "completed") 737 | if res.is_err(): 738 | return res 739 | #region dependency sniffing 740 | res = yield(NPMUtils.get_package_file_godot(p.name, p.version), "completed") 741 | if res.is_err(): 742 | return res 743 | var package_file: Dictionary = res.unwrap() 744 | for package_name in package_file.get(PackageKeys.PACKAGES, {}).keys(): 745 | var dep := Package.new(package_name, package_file[PackageKeys.PACKAGES][package_name], gpm, true) 746 | p.depend(dep) 747 | res = yield(_add(dep, {}), "completed") 748 | if res.is_err(): 749 | return res 750 | #endregion 751 | return PackageResult.ok() 752 | 753 | # @result GDScriptFunctionState> 754 | func update() -> PackageResult: 755 | var res: PackageResult = read_config(PACKAGE_FILE) 756 | if res.is_err(): 757 | return res 758 | 759 | _packages.clear() 760 | var file := File.new() 761 | var cfg = res.unwrap() 762 | for pkg in cfg.get(PackageKeys.PACKAGES, {}).keys(): 763 | var v: String = ( 764 | cfg[PackageKeys.PACKAGES][pkg][PackageKeys.VERSION] 765 | if typeof(cfg[PackageKeys.PACKAGES][pkg]) == TYPE_DICTIONARY 766 | else cfg[PackageKeys.PACKAGES][pkg] 767 | ) 768 | 769 | res = yield(_add(Package.new(pkg, v, gpm), cfg), "completed") 770 | if res.is_err(): 771 | return res 772 | 773 | res = ScriptUtils.parse_hooks(cfg) 774 | if res.is_err(): 775 | return res 776 | 777 | hooks = res.unwrap() 778 | 779 | return PackageResult.ok() 780 | 781 | #region Config handling 782 | ## Reads a config file 783 | ## 784 | ## @param: file_name: String - Either the PACKAGE_FILE or the LOCK_FILE 785 | ## 786 | ## @return: PackageResult - The contents of the config file 787 | static func read_config(file_name: String) -> PackageResult: 788 | if not file_name in [PACKAGE_FILE, LOCK_FILE]: 789 | return PackageResult.err(Error.Code.GENERIC, "Unrecognized file %s" % file_name) 790 | 791 | var res = FileUtils.read_file_to_string(file_name) 792 | if res.is_err(): 793 | return res 794 | 795 | res = Utils.json_to_dict(res.unwrap()) 796 | if res.is_err(): 797 | return res 798 | 799 | var data: Dictionary = res.unwrap() 800 | 801 | if file_name == PACKAGE_FILE and data.get(PackageKeys.PACKAGES, {}).empty(): 802 | return PackageResult.err(Error.Code.NO_PACKAGES, file_name) 803 | 804 | return PackageResult.ok(data) 805 | 806 | ## Writes a `Dictionary` to a specified file_name in the project root 807 | ## 808 | ## @param: file_name: String - Either the `PACKAGE_FILE` or the `LOCK_FILE` 809 | ## 810 | ## @result: PackageResult<()> - The result of the operation 811 | static func write_config(file_name: String, data: Dictionary) -> PackageResult: 812 | if not file_name in [PACKAGE_FILE, LOCK_FILE]: 813 | return PackageResult.err(Error.Code.GENERIC, "Unrecognized file %s" % file_name) 814 | 815 | var file := File.new() 816 | if file.open(file_name, File.WRITE) != OK: 817 | return PackageResult.err(Error.Code.FILE_OPEN_FAILURE, file_name) 818 | 819 | file.store_string(JSON.print(data, "\t")) 820 | 821 | file.close() 822 | 823 | return PackageResult.ok() 824 | 825 | #endregion 826 | 827 | func run_hook(hook: String): 828 | return hooks.run(gpm, hook) 829 | 830 | func lock() -> PackageResult: 831 | var lock_file := {} 832 | for pkg in self: 833 | if pkg.installed: 834 | lock_file[pkg.name] = { 835 | LockFileKeys.VERSION: pkg.version, 836 | LockFileKeys.INTEGRITY: pkg.integrity, 837 | } 838 | var res = write_config(LOCK_FILE, lock_file) 839 | if not res or res.is_err(): 840 | return res if res else PackageResult.err(Error.Code.GENERIC, "Unable to write configs") 841 | return PackageResult.ok() 842 | 843 | 844 | #endregion 845 | 846 | #region Script/expression handling 847 | 848 | 849 | class AdvancedExpression: 850 | class AbstractCode: 851 | var _cache := [] 852 | 853 | func _to_string() -> String: 854 | return "%s\n%s" % [_get_name(), output()] 855 | 856 | func _get_name() -> String: 857 | return "AbstractCode" 858 | 859 | static func _build_string(list: Array) -> String: 860 | return PoolStringArray(list).join("") 861 | 862 | func tab(times: int = 1) -> AbstractCode: 863 | for i in times: 864 | _cache.append("\t") 865 | return self 866 | 867 | func newline() -> AbstractCode: 868 | _cache.append("\n") 869 | return self 870 | 871 | func add(text) -> AbstractCode: 872 | match typeof(text): 873 | TYPE_STRING: 874 | tab() 875 | _cache.append(text) 876 | newline() 877 | TYPE_ARRAY: 878 | _cache.append_array(text) 879 | _: 880 | push_error("Invalid type for add: %s" % str(text)) 881 | 882 | return self 883 | 884 | func clear_cache() -> AbstractCode: 885 | _cache.clear() 886 | return self 887 | 888 | func output() -> String: 889 | return _build_string(_cache) 890 | 891 | func raw_data() -> Array: 892 | return _cache 893 | 894 | class Variable extends AbstractCode: 895 | func _init(var_name: String, var_value: String = "") -> void: 896 | _cache.append("var %s = " % var_name) 897 | if not var_value.empty(): 898 | _cache.append(var_value) 899 | 900 | func _get_name() -> String: 901 | return "Variable" 902 | 903 | func add(text) -> AbstractCode: 904 | _cache.append(str(text)) 905 | 906 | return self 907 | 908 | func output() -> String: 909 | return "%s\n" % .output() 910 | 911 | class AbstractFunction extends AbstractCode: 912 | var _function_def := "" 913 | var _params := [] 914 | 915 | func _get_name() -> String: 916 | return "AbstractFunction" 917 | 918 | func _construct_params() -> String: 919 | var params := [] 920 | params.append("(") 921 | 922 | for i in _params: 923 | params.append(i) 924 | params.append(",") 925 | 926 | # Remove the last comma 927 | if params.size() > 1: 928 | params.pop_back() 929 | 930 | params.append(")") 931 | 932 | return PoolStringArray(params).join("") if not params.empty() else "" 933 | 934 | func add_param(text: String) -> AbstractFunction: 935 | if _params.has(text): 936 | push_error("Tried to add duplicate param %s" % text) 937 | else: 938 | _params.append(text) 939 | 940 | return self 941 | 942 | func output() -> String: 943 | var params = _construct_params() 944 | var the_rest = _build_string(_cache) 945 | return "%s%s" % [_function_def % _construct_params(), _build_string(_cache)] 946 | 947 | class Function extends AbstractFunction: 948 | func _init(text: String) -> void: 949 | _function_def = "func %s%s:" % [text, "%s"] 950 | # Always add a newline into the cache 951 | newline() 952 | 953 | func _get_name() -> String: 954 | return "Function" 955 | 956 | class Runner extends AbstractFunction: 957 | func _init() -> void: 958 | _function_def = "func %s%s:" % [RUN_FUNC, "%s"] 959 | # Always add a newline into the cache 960 | newline() 961 | 962 | func _get_name() -> String: 963 | return "Runner" 964 | 965 | const RUN_FUNC := "__runner__" 966 | 967 | var variables := [] 968 | var functions := [] 969 | var runner := Runner.new() 970 | 971 | var gdscript: GDScript 972 | 973 | func _to_string() -> String: 974 | return _build_source(variables, functions, runner) 975 | 976 | static func _build_source(v: Array, f: Array, r: Runner) -> String: 977 | var source := "" 978 | 979 | for i in v: 980 | source += i.output() 981 | 982 | for i in f: 983 | source += i.output() 984 | 985 | source += r.output() 986 | 987 | return source 988 | 989 | static func _create_script(v: Array, f: Array, r: Runner) -> GDScript: 990 | var s := GDScript.new() 991 | 992 | var source := "" 993 | 994 | for i in v: 995 | source += i.output() 996 | 997 | for i in f: 998 | source += i.output() 999 | 1000 | source += r.output() 1001 | 1002 | s.source_code = source 1003 | 1004 | return s 1005 | 1006 | func add_variable(variable_name: String, variable_value: String = "") -> Variable: 1007 | var variable := Variable.new(variable_name, variable_value) 1008 | 1009 | variables.append(variable) 1010 | 1011 | return variable 1012 | 1013 | func add_function(function_name: String) -> Function: 1014 | var function := Function.new(function_name) 1015 | 1016 | functions.append(function) 1017 | 1018 | return function 1019 | 1020 | func add(text: String = "") -> Runner: 1021 | if not text.empty(): 1022 | runner.add(text) 1023 | 1024 | return runner 1025 | 1026 | func add_raw(text: String) -> Runner: 1027 | var split := text.split(";") 1028 | for i in split: 1029 | runner.add(i) 1030 | 1031 | return runner 1032 | 1033 | func tab(amount: int = 1) -> Runner: 1034 | runner.tab(amount) 1035 | 1036 | return runner 1037 | 1038 | func newline() -> Runner: 1039 | runner.newline() 1040 | 1041 | return runner 1042 | 1043 | func compile() -> int: 1044 | gdscript = _create_script(variables, functions, runner) 1045 | 1046 | return gdscript.reload() 1047 | 1048 | func execute(params: Array = []): 1049 | return gdscript.new().callv(RUN_FUNC, params) 1050 | 1051 | func clear() -> void: 1052 | gdscript = null 1053 | 1054 | variables.clear() 1055 | functions.clear() 1056 | runner = Runner.new() 1057 | 1058 | 1059 | class ScriptUtils: 1060 | static func _clean_text(text: String) -> PackageResult: 1061 | var whitespace_regex := Utils.compile_regex("\\B(\\s+)\\b") 1062 | 1063 | var r := PoolStringArray() 1064 | 1065 | var split: PoolStringArray = text.split("\n") 1066 | if split.empty(): 1067 | return PackageResult.err(Error.Code.MISSING_SCRIPT) 1068 | 1069 | var first_line := "" 1070 | for i in split: 1071 | first_line = i 1072 | if not first_line.strip_edges().empty(): 1073 | break 1074 | 1075 | # The first line should not have any tabs at all, so any tabs there can be assumed 1076 | # to also be uniformly applied to the other lines 1077 | # It's also possible spaces are used instead of tabs, so check for spaces as well 1078 | var regex_match := whitespace_regex.search(first_line) 1079 | if regex_match == null: 1080 | return PackageResult.ok(split) 1081 | 1082 | var empty_prefix: String = regex_match.get_string() 1083 | 1084 | for i in split: 1085 | # We can guarantee that the string will always be empty and, after the first iteration, 1086 | # will always have a proper new line character 1087 | r.append(i.trim_prefix(empty_prefix)) 1088 | 1089 | return PackageResult.ok(r) 1090 | 1091 | ## Build a script from a `String` 1092 | ## 1093 | ## @param: text: String - The code to build and compile 1094 | ## 1095 | ## @return: PackageResult - The compiled script 1096 | static func build_script(text: String) -> PackageResult: 1097 | var ae := AdvancedExpression.new() 1098 | 1099 | var clean_res := _clean_text(text) 1100 | if clean_res.is_err(): 1101 | return clean_res 1102 | 1103 | var split: PoolStringArray = clean_res.unwrap() 1104 | 1105 | ae.runner.add_param("gpm") 1106 | 1107 | for line in split: 1108 | ae.add(line) 1109 | 1110 | if ae.compile() != OK: 1111 | return PackageResult.err(Error.Code.SCRIPT_COMPILE_FAILURE, "build_script") 1112 | 1113 | return PackageResult.ok(ae) 1114 | 1115 | ## Parse all hooks. Failures cause the entire function to short-circuit 1116 | ## 1117 | ## @param: data: Dictionary - The entire package dictionary 1118 | ## 1119 | ## @return: PackageResult - The result of the operation 1120 | static func parse_hooks(data: Dictionary) -> PackageResult: 1121 | var hooks := Hooks.new() 1122 | 1123 | var file_hooks: Dictionary = data.get(PackageKeys.HOOKS, {}) 1124 | 1125 | for key in file_hooks.keys(): 1126 | if not key in ValidHooks.values(): 1127 | printerr("Unrecognized hook %s" % key) 1128 | continue 1129 | 1130 | var val = file_hooks[key] 1131 | match typeof(val): 1132 | TYPE_ARRAY: 1133 | var res = build_script(PoolStringArray(val).join("\n")) 1134 | if res.is_err(): 1135 | return PackageResult.err(Error.Code.SCRIPT_COMPILE_FAILURE, key) 1136 | hooks.add(key, res.unwrap()) 1137 | TYPE_DICTIONARY: 1138 | var type = val.get("type", "") 1139 | var value = val.get("value", "") 1140 | if type == "script": 1141 | var res = build_script(value) 1142 | if res.is_err(): 1143 | return PackageResult.err(Error.Code.SCRIPT_COMPILE_FAILURE, key) 1144 | hooks.add(key, res.unwrap()) 1145 | elif type == "script_name": 1146 | if not data[PackageKeys.SCRIPTS].has(value): 1147 | return PackageResult.err(Error.Code.MISSING_SCRIPT, key) 1148 | 1149 | var res = build_script(data[PackageKeys.SCRIPTS][value]) 1150 | if res.is_err(): 1151 | return PackageResult.err(Error.Code.SCRIPT_COMPILE_FAILURE, key) 1152 | hooks.add(key, res.unwrap()) 1153 | else: 1154 | return PackageResult.err(Error.Code.BAD_SCRIPT_TYPE, value) 1155 | _: 1156 | var res = build_script(val) 1157 | if res.is_err(): 1158 | return PackageResult.err(Error.Code.SCRIPT_COMPILE_FAILURE, key) 1159 | hooks.add(key, res.unwrap()) 1160 | 1161 | return PackageResult.ok(hooks) 1162 | 1163 | # parse the optional_when script, or required_when 1164 | # 1165 | # @result PackageResult 1166 | static func dict_to_script(body_data, package_file: Dictionary) -> PackageResult: 1167 | var body 1168 | if body_data is Dictionary: 1169 | var type = body_data.get("type", "") 1170 | var value = body_data.get("value", "") 1171 | if type == "script": 1172 | body = value 1173 | elif type == "script_name": 1174 | if not package_file[PackageKeys.SCRIPTS].has(value): 1175 | return PackageResult.err(Error.Code.PARSE_FAILURE, "Script does not exist %s" % value) 1176 | 1177 | body = package_file[PackageKeys.SCRIPTS][value] 1178 | else: 1179 | # Invalid type, assume the entire block is bad 1180 | return PackageResult.err(Error.Code.PARSE_FAILURE, "Invalid type %s, bailing out" % type) 1181 | else: 1182 | body = body_data 1183 | var res = build_script(body if body is String else PoolStringArray(body).join("\n")) 1184 | if res.is_err(): 1185 | return res 1186 | var code: AdvancedExpression = res.unwrap() 1187 | return PackageResult.ok(code) 1188 | 1189 | 1190 | #endregion 1191 | 1192 | ## A message that may be logged at any point during runtime 1193 | signal message_logged(text) 1194 | 1195 | ## Signifies the start of a package operation 1196 | signal operation_started(op_name, num_packages) 1197 | ## Emitted when a package has started processing 1198 | signal operation_checkpoint_reached(package_name) 1199 | ## Emitted when the package operation is complete 1200 | signal operation_finished 1201 | 1202 | #region Constants 1203 | 1204 | const REGISTRY := "https://registry.npmjs.org" 1205 | const ADDONS_DIR := "res://addons" 1206 | const ADDONS_DIR_FORMAT := ADDONS_DIR + "/%s" 1207 | const DEPENDENCIES_DIR_FORMAT := "res://addons/__gpm_deps/%s/%s" 1208 | 1209 | const DryRunValues := { 1210 | "OK": "ok", 1211 | "UPDATE": "packages_to_update", 1212 | "INVALID": "packages_with_errors" 1213 | } 1214 | 1215 | const CONNECTING_STATUS := [HTTPClient.STATUS_CONNECTING, HTTPClient.STATUS_RESOLVING] 1216 | const SUCCESS_STATUS := [ 1217 | HTTPClient.STATUS_BODY, 1218 | HTTPClient.STATUS_CONNECTED, 1219 | ] 1220 | 1221 | const HEADERS := ["User-Agent: GodotPackageManager/1.0 (you-win on GitHub)", "Accept: */*"] 1222 | 1223 | const PACKAGE_FILE := "godot.package" 1224 | const PackageKeys := { 1225 | "PACKAGES": "packages", 1226 | "HOOKS": "hooks", 1227 | "VERSION": "version", 1228 | "SCRIPTS": "scripts", 1229 | "REQUIRED_WHEN": "required_when", 1230 | "OPTIONAL_WHEN": "optional_when" 1231 | } 1232 | const LOCK_FILE := "godot.lock" 1233 | const LockFileKeys := { 1234 | "VERSION": "version", 1235 | "INTEGRITY": "integrity", 1236 | } 1237 | 1238 | const NPM_PACKAGE_FILE := "package.json" 1239 | const NpmManifestKeys := {"VERSION": "version", "DIST": "dist", "INTEGRITY": "integrity", "TARBALL": "tarball"} 1240 | const NpmPackageKeys := { 1241 | "PACKAGES": "dependencies", 1242 | } 1243 | 1244 | const ValidHooks := { 1245 | "PRE_DRY_RUN": "pre_dry_run", 1246 | "POST_DRY_RUN": "post_dry_run", 1247 | 1248 | "PRE_UPDATE": "pre_update", 1249 | "POST_UPDATE": "post_update", 1250 | 1251 | "PRE_PURGE": "pre_purge", 1252 | "POST_PURGE": "post_purge" 1253 | } 1254 | 1255 | var pkg_configs := WantedPackages.new(self) 1256 | #endregion 1257 | 1258 | ############################################################################### 1259 | # Builtin functions # 1260 | ############################################################################### 1261 | 1262 | ############################################################################### 1263 | # Connections # 1264 | ############################################################################### 1265 | 1266 | ############################################################################### 1267 | # Public functions # 1268 | ############################################################################### 1269 | 1270 | 1271 | func print(text: String) -> void: 1272 | print(text) 1273 | emit_signal("message_logged", text) 1274 | 1275 | 1276 | ## Reads the `godot.package` file and updates all packages. A `godot.lock` file is also written afterwards. 1277 | ## 1278 | ## @result: PackageResult<()> - The result of the operation 1279 | func update() -> PackageResult: 1280 | var res: PackageResult = yield(pkg_configs.update(), "completed") 1281 | if res.is_err(): 1282 | return res 1283 | 1284 | var pre_update_res = pkg_configs.run_hook(ValidHooks.PRE_UPDATE) 1285 | if typeof(pre_update_res) == TYPE_BOOL and pre_update_res == false: 1286 | emit_signal("message_logged", "Hook %s returned false" % ValidHooks.PRE_UPDATE) 1287 | return PackageResult.ok() 1288 | 1289 | var dir := Directory.new() 1290 | 1291 | emit_signal("operation_started", "update", pkg_configs.size()) 1292 | 1293 | # Used for compiling together all errors that may occur 1294 | var failed_packages := FailedPackages.new() 1295 | for pkg in pkg_configs: 1296 | res = yield(pkg.download(), "completed") 1297 | if res.is_err(): 1298 | failed_packages.add(pkg.name, str(res)) 1299 | 1300 | res = pkg_configs.lock() 1301 | if res.is_err(): 1302 | return res 1303 | 1304 | emit_signal("operation_finished") 1305 | 1306 | var post_update_res = pkg_configs.run_hook(ValidHooks.POST_UPDATE) 1307 | if typeof(post_update_res) == TYPE_BOOL and post_update_res == false: 1308 | emit_signal("message_logged", "Hook %s returned false" % ValidHooks.POST_UPDATE) 1309 | return PackageResult.ok() 1310 | 1311 | if failed_packages.has_logs(): 1312 | return PackageResult.err(Error.Code.PROCESS_PACKAGES_FAILURE, failed_packages.get_logs()) 1313 | 1314 | return PackageResult.ok() 1315 | 1316 | 1317 | func dry_run() -> PackageResult: 1318 | var res: PackageResult = yield(pkg_configs.update(), "completed") 1319 | if res.is_err(): 1320 | return res 1321 | 1322 | var pre_dry_run_res = pkg_configs.run_hook(ValidHooks.PRE_DRY_RUN) 1323 | if typeof(pre_dry_run_res) == TYPE_BOOL and pre_dry_run_res == false: 1324 | emit_signal("message_logged", "Hook %s returned false" % ValidHooks.PRE_DRY_RUN) 1325 | return PackageResult.ok({DryRunValues.OK: true}) 1326 | 1327 | emit_signal("operation_started", "dry run", pkg_configs.size()) 1328 | var failed_packages := FailedPackages.new() 1329 | var packages_to_update := [] 1330 | for package in pkg_configs: 1331 | emit_signal("operation_checkpoint_reached", package.name) 1332 | if not package.should_download(): 1333 | continue 1334 | packages_to_update.append(package.name) 1335 | 1336 | emit_signal("operation_finished") 1337 | 1338 | var post_dry_run_res = pkg_configs.run_hook(ValidHooks.POST_DRY_RUN) 1339 | if typeof(post_dry_run_res) == TYPE_BOOL and post_dry_run_res == false: 1340 | emit_signal("message_logged", "Hook %s returned false" % ValidHooks.POST_DRY_RUN) 1341 | return PackageResult.ok({DryRunValues.OK: true}) 1342 | 1343 | return PackageResult.ok({ 1344 | DryRunValues.OK: packages_to_update.empty() and failed_packages.failed_package_log.empty(), 1345 | DryRunValues.UPDATE: packages_to_update, 1346 | DryRunValues.INVALID: failed_packages.failed_package_log 1347 | }) 1348 | 1349 | ## Remove all packages listed in `godot.lock` 1350 | ## 1351 | ## @return: PackageResult<()> - The result of the operation 1352 | func purge() -> PackageResult: 1353 | var res: PackageResult = yield(pkg_configs.update(), "completed") 1354 | if res.is_err(): 1355 | return res 1356 | var pre_purge_res = pkg_configs.run_hook(ValidHooks.PRE_PURGE) 1357 | if typeof(pre_purge_res) == TYPE_BOOL and pre_purge_res == false: 1358 | emit_signal("message_logged", "Hook %s returned false" % ValidHooks.PRE_PURGE) 1359 | return PackageResult.ok() 1360 | 1361 | var dir := Directory.new() 1362 | 1363 | var installed_pkgs := [] 1364 | for p in pkg_configs: 1365 | if p.installed: 1366 | installed_pkgs.append(p) 1367 | emit_signal("operation_started", "purge", installed_pkgs.size()) 1368 | 1369 | var failed_packages := FailedPackages.new() 1370 | var completed_package_count: int = 0 1371 | for pkg in installed_pkgs: 1372 | emit_signal("operation_checkpoint_reached", pkg.name) 1373 | if dir.dir_exists(pkg.download_dir): 1374 | if DirUtils.remove_dir_recursive(pkg.download_dir) != OK: 1375 | failed_packages.add(pkg.name, "Unable to remove directory") 1376 | continue 1377 | 1378 | emit_signal("operation_finished") 1379 | 1380 | var post_purge_res = pkg_configs.run_hook(ValidHooks.POST_PURGE) 1381 | if typeof(post_purge_res) == TYPE_BOOL and post_purge_res == false: 1382 | emit_signal("message_logged", "Hook %s returned false" % ValidHooks.POST_PURGE) 1383 | return PackageResult.ok() 1384 | 1385 | return ( 1386 | PackageResult.ok() 1387 | if not failed_packages.has_logs() 1388 | else PackageResult.err(Error.Code.REMOVE_PACKAGE_DIR_FAILURE, failed_packages.get_logs()) 1389 | ) 1390 | -------------------------------------------------------------------------------- /addons/godot-package-manager/main.gd: -------------------------------------------------------------------------------- 1 | extends PanelContainer 2 | 3 | var plugin 4 | 5 | var gpm = preload("res://addons/godot-package-manager/godot_package_manager.gd").new() 6 | 7 | const PURGE_TEXT := "Purge" 8 | const CONFIRM_TEXT := "Confirm" 9 | var purge_button: Button 10 | 11 | var status: TextEdit 12 | 13 | ############################################################################### 14 | # Builtin functions # 15 | ############################################################################### 16 | 17 | func _ready() -> void: 18 | gpm.connect("operation_started", self, "_on_operation_started") 19 | gpm.connect("message_logged", self, "_on_message_logged") 20 | gpm.connect("operation_checkpoint_reached", self, "_on_operation_checkpoint_reached") 21 | gpm.connect("operation_finished", self, "_on_update_finished") 22 | 23 | status = $VBoxContainer/Status 24 | 25 | var edit_package_button := $VBoxContainer/HBoxContainer/EditPackage as Button 26 | edit_package_button.hint_tooltip = "Edit the godot.package file" 27 | edit_package_button.connect("pressed", self, "_on_edit_package") 28 | 29 | var status_button := $VBoxContainer/HBoxContainer/Status as Button 30 | status_button.hint_tooltip = "Get the current package status" 31 | status_button.connect("pressed", self, "_on_status") 32 | 33 | var update_button := $VBoxContainer/HBoxContainer/Update as Button 34 | update_button.hint_tooltip = "Update all packages, ruthlessly" 35 | update_button.connect("pressed", self, "_on_update") 36 | 37 | var clear_button := $VBoxContainer/HBoxContainer/Clear as Button 38 | clear_button.hint_tooltip = "Clear the console" 39 | clear_button.connect("pressed", self, "_on_clear", [status]) 40 | 41 | purge_button = $VBoxContainer/HBoxContainer/Purge as Button 42 | purge_button.hint_tooltip = "Delete all local packages" 43 | purge_button.connect("pressed", self, "_on_purge") 44 | purge_button.connect("mouse_exited", self, "_on_purge_reset") 45 | 46 | ############################################################################### 47 | # Connections # 48 | ############################################################################### 49 | 50 | #region Edit godot.package 51 | 52 | func _on_edit_package() -> void: 53 | var popup := WindowDialog.new() 54 | popup.window_title = "Editing res://%s" % gpm.PACKAGE_FILE 55 | popup.set_anchors_preset(PRESET_WIDE) 56 | popup.connect("modal_closed", self, "_delete", [popup]) 57 | popup.connect("popup_hide", self, "_delete", [popup]) 58 | 59 | var vbox := VBoxContainer.new() 60 | vbox.set_anchors_and_margins_preset(PRESET_WIDE) 61 | 62 | var text_edit := TextEdit.new() 63 | text_edit.draw_tabs = true 64 | text_edit.draw_spaces = true 65 | text_edit.smooth_scrolling = true 66 | text_edit.caret_blink = true 67 | text_edit.minimap_draw = true 68 | text_edit.show_line_numbers = true 69 | text_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL 70 | text_edit.size_flags_vertical = Control.SIZE_EXPAND_FILL 71 | vbox.add_child(text_edit) 72 | 73 | var status_bar := Label.new() 74 | vbox.add_child(status_bar) 75 | 76 | var save_bar := HBoxContainer.new() 77 | save_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL 78 | 79 | var save_button := Button.new() 80 | save_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER | Control.SIZE_EXPAND 81 | save_button.connect("pressed", self, "_save_edit_package", [popup, status_bar, text_edit]) 82 | save_button.text = "Save" 83 | 84 | save_bar.add_child(save_button) 85 | 86 | var discard_button := Button.new() 87 | discard_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER | Control.SIZE_EXPAND 88 | discard_button.text = "Discard changes" 89 | discard_button.connect("pressed", self, "_delete", [popup]) 90 | 91 | var spacer := Control.new() 92 | spacer.rect_min_size.y = 20 93 | 94 | save_bar.add_child(discard_button) 95 | 96 | vbox.add_child(save_bar) 97 | 98 | vbox.add_child(spacer) 99 | 100 | popup.add_child(vbox) 101 | 102 | plugin.get_editor_interface().get_base_control().add_child(popup) 103 | popup.popup_centered_ratio() 104 | 105 | var file := File.new() 106 | if file.open(gpm.PACKAGE_FILE, File.READ) != OK: 107 | _log("No res://%s file found, creating new file" % gpm.PACKAGE_FILE) 108 | return 109 | 110 | text_edit.text = file.get_as_text() 111 | file.close() 112 | 113 | func _delete(node: Node) -> void: 114 | node.queue_free() 115 | 116 | func _save_edit_package(node: Node, status_bar: Label, text_edit: TextEdit) -> void: 117 | var parse_res := JSON.parse(text_edit.text) 118 | if parse_res.error != OK: 119 | var message: String = "Syntax error detected\nLine: %d\nError: %s" % [parse_res.error_line, parse_res.error_string] 120 | status_bar.text = message 121 | _log(message) 122 | return 123 | 124 | if not parse_res.result is Dictionary: 125 | var message: String = "%s must be a Dictionary" % gpm.PACKAGE_FILE 126 | status_bar.text = message 127 | _log(message) 128 | return 129 | 130 | var file := File.new() 131 | if file.open(gpm.PACKAGE_FILE, File.WRITE) != OK: 132 | _log("Unable to open %s for writing" % gpm.PACKAGE_FILE) 133 | return 134 | 135 | file.store_string(text_edit.text) 136 | 137 | file.close() 138 | 139 | _log("%s saved successfully" % gpm.PACKAGE_FILE) 140 | node.queue_free() 141 | 142 | #endregion 143 | 144 | func _on_status() -> void: 145 | _log("Getting package status") 146 | var res = yield(gpm.dry_run(), "completed") 147 | if res.is_ok(): 148 | var data: Dictionary = res.unwrap() 149 | if data.get(gpm.DryRunValues.OK, false): 150 | _log("All packages okay, no update required") 151 | return 152 | 153 | if not data.get(gpm.DryRunValues.UPDATE, []).empty(): 154 | _log("Packages to update:") 155 | for i in data[gpm.DryRunValues.UPDATE]: 156 | _log(i) 157 | if not data.get(gpm.DryRunValues.INVALID, []).empty(): 158 | _log("Invalid packages:") 159 | for i in data[gpm.DryRunValues.INVALID]: 160 | _log(i) 161 | else: 162 | _log(res.unwrap_err().to_string()) 163 | 164 | func _on_update() -> void: 165 | _log("Updating all valid packages") 166 | var res = yield(gpm.update(), "completed") 167 | if res.is_ok(): 168 | _log("Update successful") 169 | else: 170 | _log(res.unwrap_err().to_string()) 171 | 172 | func _on_clear(text_edit: TextEdit) -> void: 173 | text_edit.text = "" 174 | 175 | func _on_purge() -> void: 176 | if purge_button.text == PURGE_TEXT: 177 | purge_button.text = CONFIRM_TEXT 178 | else: 179 | _log("Purging all downloaded packages") 180 | gpm.purge() 181 | 182 | func _on_purge_reset() -> void: 183 | purge_button.text = PURGE_TEXT 184 | 185 | func _on_message_logged(text: String) -> void: 186 | _log(text) 187 | 188 | #region Update 189 | 190 | func _on_operation_started(operation: String, num_packages: int) -> void: 191 | _log("Running %s for %d packages" % [operation, num_packages]) 192 | 193 | func _on_operation_checkpoint_reached(package_name: String) -> void: 194 | _log("Processing %s" % package_name) 195 | 196 | func _on_update_finished() -> void: 197 | plugin.get_editor_interface().get_resource_filesystem().scan() 198 | _log("Finished") 199 | 200 | #endregion 201 | 202 | ############################################################################### 203 | # Private functions # 204 | ############################################################################### 205 | 206 | func _log(text: String) -> void: 207 | status.text = "%s%s\n" % [status.text, text] 208 | status.cursor_set_line(status.get_line_count()) 209 | 210 | ############################################################################### 211 | # Public functions # 212 | ############################################################################### 213 | -------------------------------------------------------------------------------- /addons/godot-package-manager/main.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://addons/godot-package-manager/main.gd" type="Script" id=1] 4 | 5 | [sub_resource type="StyleBoxEmpty" id=1] 6 | 7 | [node name="Main" type="PanelContainer"] 8 | anchor_right = 1.0 9 | anchor_bottom = 1.0 10 | rect_min_size = Vector2( 0, 200 ) 11 | script = ExtResource( 1 ) 12 | 13 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 14 | margin_left = 7.0 15 | margin_top = 7.0 16 | margin_right = 1017.0 17 | margin_bottom = 593.0 18 | 19 | [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] 20 | margin_right = 1010.0 21 | margin_bottom = 20.0 22 | 23 | [node name="EditPackage" type="Button" parent="VBoxContainer/HBoxContainer"] 24 | margin_right = 131.0 25 | margin_bottom = 20.0 26 | text = "Edit godot.package" 27 | 28 | [node name="Status" type="Button" parent="VBoxContainer/HBoxContainer"] 29 | margin_left = 135.0 30 | margin_right = 197.0 31 | margin_bottom = 20.0 32 | text = "Dry Run" 33 | 34 | [node name="Update" type="Button" parent="VBoxContainer/HBoxContainer"] 35 | margin_left = 201.0 36 | margin_right = 259.0 37 | margin_bottom = 20.0 38 | text = "Update" 39 | 40 | [node name="Clear" type="Button" parent="VBoxContainer/HBoxContainer"] 41 | margin_left = 263.0 42 | margin_right = 307.0 43 | margin_bottom = 20.0 44 | text = "Clear" 45 | 46 | [node name="Purge" type="Button" parent="VBoxContainer/HBoxContainer"] 47 | margin_left = 962.0 48 | margin_right = 1010.0 49 | margin_bottom = 20.0 50 | size_flags_horizontal = 10 51 | text = "Purge" 52 | 53 | [node name="Status" type="TextEdit" parent="VBoxContainer"] 54 | margin_top = 24.0 55 | margin_right = 1010.0 56 | margin_bottom = 586.0 57 | size_flags_horizontal = 3 58 | size_flags_vertical = 3 59 | custom_colors/font_color_readonly = Color( 1, 1, 1, 1 ) 60 | custom_styles/read_only = SubResource( 1 ) 61 | readonly = true 62 | -------------------------------------------------------------------------------- /addons/godot-package-manager/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="GodotPackageManager" 4 | description="" 5 | author="Tim Yuen" 6 | version="0.0.1" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/godot-package-manager/plugin.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | const Main := preload("res://addons/godot-package-manager/main.tscn") 5 | const GPM := preload("res://addons/godot-package-manager/godot_package_manager.gd") 6 | 7 | const TOOLBAR_NAME := "Package Manager" 8 | 9 | var main 10 | 11 | func _enter_tree(): 12 | main = Main.instance() 13 | inject_tool(main) 14 | main.plugin = self 15 | 16 | add_control_to_bottom_panel(main, TOOLBAR_NAME) 17 | 18 | func _exit_tree(): 19 | if main != null: 20 | remove_control_from_bottom_panel(main) 21 | main.free() 22 | 23 | func inject_tool(node: Node) -> void: 24 | """ 25 | Inject `tool` at the top of the plugin script 26 | """ 27 | var script: Script = node.get_script().duplicate() 28 | script.source_code = "tool\n%s" % script.source_code 29 | script.reload(false) 30 | node.set_script(script) 31 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | -------------------------------------------------------------------------------- /godot.lock: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /godot.package: -------------------------------------------------------------------------------- 1 | { 2 | "packages": {} 3 | } 4 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godot-package-manager/godot-plugin/d0a76f335cd0af7d298da42c195e8b26d1406421/icon.png -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | process/normal_map_invert_y=false 32 | stream=false 33 | size_limit=0 34 | detect_3d=true 35 | svg/scale=1.0 36 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | [application] 12 | 13 | config/name="Godot Package Manager" 14 | run/main_scene="res://runner/runner.tscn" 15 | config/icon="res://icon.png" 16 | 17 | [editor_plugins] 18 | 19 | enabled=PoolStringArray( "res://addons/godot-package-manager/plugin.cfg" ) 20 | 21 | [physics] 22 | 23 | common/enable_pause_aware_picking=true 24 | 25 | [rendering] 26 | 27 | quality/driver/driver_name="GLES2" 28 | environment/default_environment="res://default_env.tres" 29 | -------------------------------------------------------------------------------- /runner/dummy_plugin.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | func get_editor_interface() -> Node: 4 | return self 5 | 6 | func get_base_control() -> Node: 7 | return get_parent() 8 | 9 | func get_resource_filesystem() -> Node: 10 | return self 11 | 12 | func scan() -> void: 13 | return 14 | -------------------------------------------------------------------------------- /runner/main.theme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godot-package-manager/godot-plugin/d0a76f335cd0af7d298da42c195e8b26d1406421/runner/main.theme -------------------------------------------------------------------------------- /runner/runner.gd: -------------------------------------------------------------------------------- 1 | extends CanvasLayer 2 | 3 | const Main := preload("res://addons/godot-package-manager/main.tscn") 4 | const DummyPlugin := preload("res://runner/dummy_plugin.gd") 5 | 6 | ############################################################################### 7 | # Builtin functions # 8 | ############################################################################### 9 | 10 | func _ready() -> void: 11 | OS.center_window() 12 | 13 | var dummy_plugin := DummyPlugin.new() 14 | add_child(dummy_plugin) 15 | 16 | var main = Main.instance() 17 | main.plugin = dummy_plugin 18 | add_child(main) 19 | 20 | var theme = load("res://runner/main.theme") 21 | main.theme = theme 22 | 23 | ############################################################################### 24 | # Connections # 25 | ############################################################################### 26 | 27 | ############################################################################### 28 | # Private functions # 29 | ############################################################################### 30 | 31 | ############################################################################### 32 | # Public functions # 33 | ############################################################################### 34 | -------------------------------------------------------------------------------- /runner/runner.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://runner/runner.gd" type="Script" id=1] 4 | 5 | [node name="Runner" type="CanvasLayer"] 6 | script = ExtResource( 1 ) 7 | 8 | [node name="Background" type="ColorRect" parent="."] 9 | anchor_right = 1.0 10 | anchor_bottom = 1.0 11 | color = Color( 0.2, 0.227451, 0.309804, 1 ) 12 | --------------------------------------------------------------------------------