mxrb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.de-DE.md +108 -0
- data/README.md +172 -0
- data/README.pt-BR.md +109 -0
- data/bin/mxrb +1613 -0
- data/docs/README.md +10 -0
- data/docs/de-DE/README.md +24 -0
- data/docs/de-DE/architectural-patterns.md +26 -0
- data/docs/de-DE/architectural-standard.md +48 -0
- data/docs/de-DE/architecture.md +76 -0
- data/docs/de-DE/compiler.md +162 -0
- data/docs/de-DE/conventions.md +19 -0
- data/docs/de-DE/design-system.md +52 -0
- data/docs/de-DE/entity-dsl.md +53 -0
- data/docs/de-DE/native-runtime-quality-report.md +74 -0
- data/docs/de-DE/oql-sql.md +211 -0
- data/docs/de-DE/platform-api-integration.md +36 -0
- data/docs/de-DE/platform-operations.md +243 -0
- data/docs/de-DE/project-structure.md +32 -0
- data/docs/de-DE/ruby-first-roadmap.md +116 -0
- data/docs/de-DE/scaffolds.md +59 -0
- data/docs/de-DE/semantic-refactoring.md +56 -0
- data/docs/de-DE/team-server.md +33 -0
- data/docs/de-DE/validation-matrix.md +64 -0
- data/docs/de-DE/vetclinic-acceptance.md +24 -0
- data/docs/de-DE/writing.md +138 -0
- data/docs/en-US/README.md +24 -0
- data/docs/en-US/architectural-patterns.md +151 -0
- data/docs/en-US/architectural-standard.md +89 -0
- data/docs/en-US/architecture.md +170 -0
- data/docs/en-US/compiler.md +157 -0
- data/docs/en-US/conventions.md +82 -0
- data/docs/en-US/design-system.md +163 -0
- data/docs/en-US/entity-dsl.md +53 -0
- data/docs/en-US/native-runtime-quality-report.md +73 -0
- data/docs/en-US/oql-sql.md +201 -0
- data/docs/en-US/platform-api-integration.md +36 -0
- data/docs/en-US/platform-operations.md +244 -0
- data/docs/en-US/project-structure.md +49 -0
- data/docs/en-US/ruby-first-roadmap.md +186 -0
- data/docs/en-US/scaffolds.md +57 -0
- data/docs/en-US/semantic-refactoring.md +198 -0
- data/docs/en-US/team-server.md +32 -0
- data/docs/en-US/validation-matrix.md +257 -0
- data/docs/en-US/vetclinic-acceptance.md +23 -0
- data/docs/en-US/writing.md +655 -0
- data/docs/pt-BR/README.md +24 -0
- data/docs/pt-BR/architectural-patterns.md +25 -0
- data/docs/pt-BR/architectural-standard.md +533 -0
- data/docs/pt-BR/architecture.md +300 -0
- data/docs/pt-BR/compiler.md +162 -0
- data/docs/pt-BR/conventions.md +19 -0
- data/docs/pt-BR/design-system.md +65 -0
- data/docs/pt-BR/entity-dsl.md +116 -0
- data/docs/pt-BR/native-runtime-quality-report.md +73 -0
- data/docs/pt-BR/oql-sql.md +208 -0
- data/docs/pt-BR/platform-api-integration.md +36 -0
- data/docs/pt-BR/platform-operations.md +260 -0
- data/docs/pt-BR/project-structure.md +32 -0
- data/docs/pt-BR/ruby-first-roadmap.md +239 -0
- data/docs/pt-BR/scaffolds.md +104 -0
- data/docs/pt-BR/semantic-refactoring.md +55 -0
- data/docs/pt-BR/team-server.md +59 -0
- data/docs/pt-BR/validation-matrix.md +86 -0
- data/docs/pt-BR/vetclinic-acceptance.md +50 -0
- data/docs/pt-BR/writing.md +177 -0
- data/examples/sudoku_evaluation.rb +14 -0
- data/examples/sudoku_functional_test.rb +28 -0
- data/lib/mxrb/architecture/graph.rb +124 -0
- data/lib/mxrb/architecture/validator.rb +98 -0
- data/lib/mxrb/benchmark.rb +33 -0
- data/lib/mxrb/compare.rb +390 -0
- data/lib/mxrb/compiler/adapter.rb +96 -0
- data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
- data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
- data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
- data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
- data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
- data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
- data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
- data/lib/mxrb/compiler/constants_materializer.rb +111 -0
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
- data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
- data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
- data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
- data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
- data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
- data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
- data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
- data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
- data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
- data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
- data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
- data/lib/mxrb/compiler/mda.rb +99 -0
- data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
- data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
- data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
- data/lib/mxrb/compiler/model_package.rb +116 -0
- data/lib/mxrb/compiler/model_values.rb +67 -0
- data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
- data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
- data/lib/mxrb/compiler/packager.rb +100 -0
- data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
- data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
- data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
- data/lib/mxrb/compiler/portable_packager.rb +356 -0
- data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
- data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
- data/lib/mxrb/compiler/project_materializer.rb +139 -0
- data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
- data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
- data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
- data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
- data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
- data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
- data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
- data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
- data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
- data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
- data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
- data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
- data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
- data/lib/mxrb/compiler/security_materializer.rb +104 -0
- data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
- data/lib/mxrb/compiler/settings_materializer.rb +25 -0
- data/lib/mxrb/compiler/source_model.rb +128 -0
- data/lib/mxrb/compiler/system_model_seed.rb +88 -0
- data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
- data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
- data/lib/mxrb/compiler/translation_materializer.rb +95 -0
- data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
- data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
- data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
- data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
- data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
- data/lib/mxrb/doctor.rb +116 -0
- data/lib/mxrb/dsl/builder.rb +1636 -0
- data/lib/mxrb/errors.rb +18 -0
- data/lib/mxrb/evaluation.rb +131 -0
- data/lib/mxrb/exporter.rb +1716 -0
- data/lib/mxrb/frontend/migrator.rb +775 -0
- data/lib/mxrb/functional.rb +307 -0
- data/lib/mxrb/github/annotator.rb +161 -0
- data/lib/mxrb/initializer.rb +257 -0
- data/lib/mxrb/integrity/validator.rb +136 -0
- data/lib/mxrb/io/bson_codec.rb +145 -0
- data/lib/mxrb/io/mpr_file.rb +558 -0
- data/lib/mxrb/io/mxunit_codec.rb +42 -0
- data/lib/mxrb/marketplace.rb +380 -0
- data/lib/mxrb/model/association.rb +98 -0
- data/lib/mxrb/model/attribute.rb +100 -0
- data/lib/mxrb/model/connector.rb +17 -0
- data/lib/mxrb/model/design_materializer.rb +133 -0
- data/lib/mxrb/model/design_migration.rb +126 -0
- data/lib/mxrb/model/design_system.rb +111 -0
- data/lib/mxrb/model/domain_model.rb +51 -0
- data/lib/mxrb/model/entity.rb +180 -0
- data/lib/mxrb/model/menu.rb +38 -0
- data/lib/mxrb/model/microflow.rb +100 -0
- data/lib/mxrb/model/module.rb +142 -0
- data/lib/mxrb/model/navigation.rb +127 -0
- data/lib/mxrb/model/page.rb +410 -0
- data/lib/mxrb/model/project.rb +187 -0
- data/lib/mxrb/model/unit.rb +48 -0
- data/lib/mxrb/module_initializer.rb +75 -0
- data/lib/mxrb/official_marketplace/content_api.rb +292 -0
- data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
- data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
- data/lib/mxrb/official_marketplace.rb +720 -0
- data/lib/mxrb/oql/analyzer.rb +155 -0
- data/lib/mxrb/oql/index_advisor.rb +85 -0
- data/lib/mxrb/oql/plan_analyzer.rb +185 -0
- data/lib/mxrb/oql/server.rb +132 -0
- data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
- data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
- data/lib/mxrb/oql/workload_analyzer.rb +175 -0
- data/lib/mxrb/oql/workload_baseline.rb +60 -0
- data/lib/mxrb/oql.rb +424 -0
- data/lib/mxrb/project_lifecycle.rb +76 -0
- data/lib/mxrb/protocols/plan.rb +86 -0
- data/lib/mxrb/protocols.rb +123 -0
- data/lib/mxrb/runtime/database_workspace.rb +677 -0
- data/lib/mxrb/runtime/docker_executor.rb +41 -0
- data/lib/mxrb/runtime/docker_workspace.rb +29 -0
- data/lib/mxrb/runtime/executor.rb +169 -0
- data/lib/mxrb/runtime/java_locator.rb +26 -0
- data/lib/mxrb/runtime/native.rb +474 -0
- data/lib/mxrb/runtime/sql_server_database.rb +146 -0
- data/lib/mxrb/runtime/toolchain.rb +82 -0
- data/lib/mxrb/scaffold/cli.rb +113 -0
- data/lib/mxrb/scaffold/generator.rb +246 -0
- data/lib/mxrb/scaffold/help.rb +80 -0
- data/lib/mxrb/scaffold/page_templates.rb +54 -0
- data/lib/mxrb/scaffold/recipes.rb +198 -0
- data/lib/mxrb/scaffold/registry.rb +82 -0
- data/lib/mxrb/scaffold/templates.rb +723 -0
- data/lib/mxrb/scaffold/transaction.rb +84 -0
- data/lib/mxrb/schema/tables.rb +55 -0
- data/lib/mxrb/semantic/analyzer.rb +473 -0
- data/lib/mxrb/semantic/batch_plan.rb +68 -0
- data/lib/mxrb/semantic/domain_mutator.rb +419 -0
- data/lib/mxrb/semantic/embedder.rb +51 -0
- data/lib/mxrb/semantic/extractor.rb +290 -0
- data/lib/mxrb/semantic/index.rb +623 -0
- data/lib/mxrb/semantic/inliner.rb +216 -0
- data/lib/mxrb/semantic/mover.rb +153 -0
- data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
- data/lib/mxrb/semantic/remover.rb +70 -0
- data/lib/mxrb/semantic/renamer.rb +211 -0
- data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
- data/lib/mxrb/semantic/vec_store.rb +75 -0
- data/lib/mxrb/team_server.rb +390 -0
- data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
- data/lib/mxrb/templates/project/11.12.1.json +32 -0
- data/lib/mxrb/templates/project/6.10.8.json +50 -0
- data/lib/mxrb/templates/project/7.17.0.json +50 -0
- data/lib/mxrb/templates/project/7.5.0.json +50 -0
- data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
- data/lib/mxrb/version.rb +5 -0
- data/lib/mxrb/widget_package.rb +338 -0
- data/lib/mxrb/widget_synchronizer.rb +31 -0
- data/lib/mxrb/writer.rb +3775 -0
- data/lib/mxrb.rb +188 -0
- data/marketplace/catalog.json +10 -0
- data/marketplace/modules/shared-kernel/domain/README.md +4 -0
- data/marketplace/modules/shared-kernel/module.rb +5 -0
- data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
- metadata +447 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'date'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'net/http'
|
|
8
|
+
require 'tmpdir'
|
|
9
|
+
require 'uri'
|
|
10
|
+
require 'zip'
|
|
11
|
+
require_relative 'official_marketplace/module_package_importer'
|
|
12
|
+
require_relative 'official_marketplace/widget_package_installer'
|
|
13
|
+
require_relative 'official_marketplace/lifecycle'
|
|
14
|
+
require_relative 'official_marketplace/dependency_resolver'
|
|
15
|
+
|
|
16
|
+
module Mxrb
|
|
17
|
+
# Installer for official/community Mendix packages, separate from Ruby modules.
|
|
18
|
+
module OfficialMarketplace
|
|
19
|
+
Package = Data.define(:name, :version, :source, :download_url, :repository)
|
|
20
|
+
OfficialPackage = Data.define(
|
|
21
|
+
:name, :version, :source, :download_url, :repository, :content_id, :version_id,
|
|
22
|
+
:content_type, :version_type, :security_issues, :private, :company_approved
|
|
23
|
+
)
|
|
24
|
+
Installation = Data.define(:package, :destination, :sha256)
|
|
25
|
+
ModuleInstallation = Data.define(
|
|
26
|
+
:package, :destination, :sha256, :module_name, :module_id, :units, :files, :archive
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
CATALOG = {
|
|
30
|
+
'CommunityCommons' => 'mendix/CommunityCommons',
|
|
31
|
+
'UnitTesting' => 'mendix/UnitTesting'
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
def self.tree_digest(path)
|
|
35
|
+
digest = Digest::SHA256.new
|
|
36
|
+
Dir.glob(File.join(path, '**', '*'), File::FNM_DOTMATCH).sort.each do |file|
|
|
37
|
+
next unless File.file?(file)
|
|
38
|
+
|
|
39
|
+
digest << file.delete_prefix("#{path}/") << "\0" << File.binread(file)
|
|
40
|
+
end
|
|
41
|
+
digest.hexdigest
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.lock(target)
|
|
45
|
+
path = File.join(File.expand_path(target), '.mxrb', 'marketplace.lock.json')
|
|
46
|
+
File.file?(path) ? JSON.parse(File.read(path)) : { 'packages' => {} }
|
|
47
|
+
rescue JSON::ParserError => e
|
|
48
|
+
raise MarketplaceError, "invalid marketplace lockfile: #{e.message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.verify(target)
|
|
52
|
+
root = File.expand_path(target)
|
|
53
|
+
lock(root).fetch('packages').to_h do |name, entry|
|
|
54
|
+
result = case entry['kind']
|
|
55
|
+
when 'module' then verify_module(root, name, entry)
|
|
56
|
+
when 'widget' then verify_widget(root, entry)
|
|
57
|
+
else verify_tree(root, entry)
|
|
58
|
+
end
|
|
59
|
+
[name, result]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
64
|
+
def self.verify_widget(root, entry)
|
|
65
|
+
destination = File.join(root, entry.fetch('destination'))
|
|
66
|
+
archive = File.join(root, entry.fetch('archive'))
|
|
67
|
+
actual = File.file?(destination) ? Digest::SHA256.file(destination).hexdigest : nil
|
|
68
|
+
cached = File.file?(archive) ? Digest::SHA256.file(archive).hexdigest : nil
|
|
69
|
+
inventory = WidgetPackageInventory.read(destination) if actual == entry['sha256']
|
|
70
|
+
identity = inventory&.name == entry['widget_name'] && inventory&.widget_ids == entry['widget_ids']
|
|
71
|
+
{
|
|
72
|
+
valid: actual == entry['sha256'] && cached == entry['sha256'] && identity,
|
|
73
|
+
expected: entry['sha256'], actual:, cached:, identity:
|
|
74
|
+
}
|
|
75
|
+
rescue MarketplaceError
|
|
76
|
+
{ valid: false, expected: entry['sha256'], actual:, cached:, identity: false }
|
|
77
|
+
end
|
|
78
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
79
|
+
|
|
80
|
+
def self.verify_tree(root, entry)
|
|
81
|
+
destination = File.join(root, entry.fetch('destination'))
|
|
82
|
+
actual = File.directory?(destination) ? tree_digest(destination) : nil
|
|
83
|
+
{ valid: actual == entry['sha256'], expected: entry['sha256'], actual: }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.verify_module(root, name, entry) # rubocop:disable Metrics/AbcSize
|
|
87
|
+
archive = File.join(root, entry.fetch('archive'))
|
|
88
|
+
actual = File.file?(archive) ? Digest::SHA256.file(archive).hexdigest : nil
|
|
89
|
+
mpr_path = File.join(root, entry.fetch('destination'))
|
|
90
|
+
present = module_present?(mpr_path, name, entry['module_id'])
|
|
91
|
+
files_present = Array(entry['files']).all? { File.file?(File.join(root, _1)) }
|
|
92
|
+
{
|
|
93
|
+
valid: actual == entry['sha256'] && present && files_present,
|
|
94
|
+
expected: entry['sha256'], actual:, module_present: present, files_present:
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.module_present?(mpr_path, name, module_id)
|
|
99
|
+
return false unless File.file?(mpr_path)
|
|
100
|
+
|
|
101
|
+
mpr = IO::MprFile.open(mpr_path, readonly: true)
|
|
102
|
+
unit = module_id ? mpr.unit(module_id) : module_by_name(mpr, name)
|
|
103
|
+
unit && mpr.parse_contents(unit)['Name'] == name
|
|
104
|
+
rescue Error, SQLite3::Exception
|
|
105
|
+
false
|
|
106
|
+
ensure
|
|
107
|
+
mpr&.close
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.module_by_name(mpr, name)
|
|
111
|
+
mpr.units_by_containment('Modules').find { mpr.parse_contents(_1)['Name'] == name }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Resolves a user-owned PAT file. Embedding the PAT remains an explicit,
|
|
115
|
+
# backwards-compatible opt-in for users who prefer MXRB-managed storage.
|
|
116
|
+
class Credentials
|
|
117
|
+
attr_reader :path
|
|
118
|
+
|
|
119
|
+
def initialize(path: nil, pat_file: ENV['MXRB_MENDIX_PAT_FILE'])
|
|
120
|
+
base = ENV['XDG_CONFIG_HOME'].to_s
|
|
121
|
+
base = File.join(Dir.home, '.config') if base.empty?
|
|
122
|
+
@path = path || File.join(base, 'mxrb', 'credentials')
|
|
123
|
+
@pat_file = expand_pat_file(pat_file)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def configure_pat_file(source)
|
|
127
|
+
@pat_file = expand_pat_file(source)
|
|
128
|
+
raise MarketplaceError, 'Mendix PAT file path must not be empty' unless @pat_file
|
|
129
|
+
|
|
130
|
+
read_pat_file(@pat_file)
|
|
131
|
+
write_config('mendix_pat_file' => @pat_file)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def save_pat(token)
|
|
135
|
+
value = token.to_s.strip
|
|
136
|
+
raise MarketplaceError, 'Mendix PAT must not be empty' if value.empty?
|
|
137
|
+
|
|
138
|
+
@pat_file = nil
|
|
139
|
+
write_config('mendix_pat' => value)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def pat
|
|
143
|
+
return read_pat_file(@pat_file) if @pat_file
|
|
144
|
+
return unless File.file?(@path)
|
|
145
|
+
|
|
146
|
+
config = read_config
|
|
147
|
+
source = expand_pat_file(config['mendix_pat_file'])
|
|
148
|
+
return read_pat_file(source) if source
|
|
149
|
+
|
|
150
|
+
config['mendix_pat']
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
def write_config(config)
|
|
156
|
+
FileUtils.mkdir_p(File.dirname(@path), mode: 0o700)
|
|
157
|
+
temporary = "#{@path}.tmp-#{Process.pid}"
|
|
158
|
+
File.binwrite(temporary, JSON.generate(config) << "\n")
|
|
159
|
+
File.chmod(0o600, temporary)
|
|
160
|
+
File.rename(temporary, @path)
|
|
161
|
+
@path
|
|
162
|
+
ensure
|
|
163
|
+
FileUtils.rm_f(temporary) if defined?(temporary) && temporary && File.exist?(temporary)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def read_config
|
|
167
|
+
value = JSON.parse(File.binread(@path))
|
|
168
|
+
raise MarketplaceError, 'invalid credentials file: expected a JSON object' unless value.is_a?(Hash)
|
|
169
|
+
|
|
170
|
+
value
|
|
171
|
+
rescue JSON::ParserError => e
|
|
172
|
+
raise MarketplaceError, "invalid credentials file: #{e.message}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def expand_pat_file(value)
|
|
176
|
+
source = value.to_s.strip
|
|
177
|
+
source.empty? ? nil : File.expand_path(source)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def read_pat_file(source)
|
|
181
|
+
raise MarketplaceError, "Mendix PAT file not found: #{source}" unless File.file?(source)
|
|
182
|
+
|
|
183
|
+
raw = File.binread(source).strip
|
|
184
|
+
value = pat_from_file_contents(raw, source)
|
|
185
|
+
raise MarketplaceError, "Mendix PAT file is empty: #{source}" if value.empty?
|
|
186
|
+
|
|
187
|
+
value
|
|
188
|
+
rescue SystemCallError => e
|
|
189
|
+
raise MarketplaceError, "cannot read Mendix PAT file #{source}: #{e.message}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def pat_from_file_contents(raw, source)
|
|
193
|
+
return pat_from_env(raw, source) if env_file?(source) || raw.match?(/^\s*(?:export\s+)?MXRB_MENDIX_PAT\s*=/)
|
|
194
|
+
|
|
195
|
+
value = JSON.parse(raw)
|
|
196
|
+
return value['mendix_pat'].to_s.strip if value.is_a?(Hash) && value.key?('mendix_pat')
|
|
197
|
+
|
|
198
|
+
raise MarketplaceError, 'JSON Mendix PAT file must contain "mendix_pat"'
|
|
199
|
+
rescue JSON::ParserError
|
|
200
|
+
raw
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def env_file?(source)
|
|
204
|
+
File.basename(source).start_with?('.env')
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def pat_from_env(raw, source)
|
|
208
|
+
line = raw.lines.reverse.find { _1.match?(/^\s*(?:export\s+)?MXRB_MENDIX_PAT\s*=/) }
|
|
209
|
+
raise MarketplaceError, "MXRB_MENDIX_PAT not found in #{source}" unless line
|
|
210
|
+
|
|
211
|
+
value = line.sub(/^\s*(?:export\s+)?MXRB_MENDIX_PAT\s*=\s*/, '').strip
|
|
212
|
+
value = value[1...-1] if value.length >= 2 && %w[' "].include?(value[0]) && value[-1] == value[0]
|
|
213
|
+
value.strip
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Small HTTPS-only GitHub client with bounded redirects and response size.
|
|
218
|
+
class HttpClient
|
|
219
|
+
MAX_BYTES = 512 * 1024 * 1024
|
|
220
|
+
|
|
221
|
+
def initialize(github_token: ENV['GITHUB_TOKEN'])
|
|
222
|
+
@github_token = github_token.to_s
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def json(url, authorization: default_authorization)
|
|
226
|
+
JSON.parse(get(url, accept: 'application/json', authorization:))
|
|
227
|
+
rescue JSON::ParserError => e
|
|
228
|
+
raise MarketplaceError, "invalid JSON response: #{e.message}"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def download(url, destination, authorization: default_authorization)
|
|
232
|
+
File.binwrite(destination, get(url, accept: 'application/octet-stream', authorization:))
|
|
233
|
+
destination
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
def get(url, accept:, redirects: 5, authorization: default_authorization,
|
|
239
|
+
authorization_host: nil)
|
|
240
|
+
raise MarketplaceError, 'too many marketplace redirects' if redirects.negative?
|
|
241
|
+
|
|
242
|
+
uri = https_uri(url)
|
|
243
|
+
authorization_host ||= uri.host
|
|
244
|
+
scoped_authorization = authorization if uri.host == authorization_host
|
|
245
|
+
response = perform(uri, accept, scoped_authorization)
|
|
246
|
+
return response_body(response) unless response.is_a?(Net::HTTPRedirection)
|
|
247
|
+
|
|
248
|
+
options = { accept:, redirects: redirects - 1, authorization:, authorization_host: }
|
|
249
|
+
follow_redirect(uri, response, options)
|
|
250
|
+
rescue URI::InvalidURIError, SocketError, SystemCallError => e
|
|
251
|
+
raise MarketplaceError, "marketplace request failed: #{e.message}"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def follow_redirect(uri, response, options)
|
|
255
|
+
get(
|
|
256
|
+
redirect_url(uri, response['location']), **options
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def redirect_url(origin, location)
|
|
261
|
+
target = URI.join(origin, location)
|
|
262
|
+
target.scheme = 'https' if origin.is_a?(URI::HTTPS) && target.host == origin.host && target.scheme == 'http'
|
|
263
|
+
target.to_s
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def https_uri(url)
|
|
267
|
+
URI.parse(url).tap do |uri|
|
|
268
|
+
raise MarketplaceError, 'marketplace downloads require HTTPS' unless uri.is_a?(URI::HTTPS)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def perform(uri, accept, authorization = default_authorization)
|
|
273
|
+
request = Net::HTTP::Get.new(uri)
|
|
274
|
+
request['Accept'] = accept
|
|
275
|
+
request['User-Agent'] = "mxrb/#{Mxrb::VERSION}"
|
|
276
|
+
request['Authorization'] = authorization unless authorization.to_s.empty?
|
|
277
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) { _1.request(request) }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def default_authorization
|
|
281
|
+
"Bearer #{@github_token}" unless @github_token.empty?
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def response_body(response)
|
|
285
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
286
|
+
raise MarketplaceError, "marketplace request failed with HTTP #{response.code}"
|
|
287
|
+
end
|
|
288
|
+
raise MarketplaceError, 'marketplace response exceeds 512 MiB' \
|
|
289
|
+
if response.body.to_s.bytesize > MAX_BYTES
|
|
290
|
+
|
|
291
|
+
response.body
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
require_relative 'official_marketplace/content_api'
|
|
296
|
+
|
|
297
|
+
# Resolves known names or explicit github:org/repo releases.
|
|
298
|
+
class GitHubResolver
|
|
299
|
+
def initialize(client: HttpClient.new)
|
|
300
|
+
@client = client
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def search(query)
|
|
304
|
+
local = catalog_search(query)
|
|
305
|
+
return local unless local.empty?
|
|
306
|
+
|
|
307
|
+
github_search(query)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def catalog_search(query)
|
|
311
|
+
term = query.to_s.downcase
|
|
312
|
+
CATALOG.filter_map do |name, repository|
|
|
313
|
+
{ name:, repository: } if name.downcase.include?(term) || repository.downcase.include?(term)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def github_search(query)
|
|
318
|
+
payload = @client.json(
|
|
319
|
+
"https://api.github.com/search/repositories?q=#{URI.encode_www_form_component(query)}+org:mendix"
|
|
320
|
+
)
|
|
321
|
+
Array(payload['items']).first(20).map do |item|
|
|
322
|
+
{ name: item['name'], repository: item['full_name'], description: item['description'] }
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def resolve(identifier, version: nil)
|
|
327
|
+
repository, name = repository_and_name(identifier)
|
|
328
|
+
endpoint = release_endpoint(version)
|
|
329
|
+
release = @client.json("https://api.github.com/repos/#{repository}/#{endpoint}")
|
|
330
|
+
download_url = release_download(release)
|
|
331
|
+
Package.new(name, release['tag_name'] || version || 'latest', :github, download_url, repository)
|
|
332
|
+
rescue KeyError => e
|
|
333
|
+
raise MarketplaceError, "GitHub release is incomplete: #{e.message}"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
private
|
|
337
|
+
|
|
338
|
+
def release_endpoint(version)
|
|
339
|
+
version ? "releases/tags/#{URI.encode_www_form_component(version)}" : 'releases/latest'
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def release_download(release)
|
|
343
|
+
assets = Array(release['assets'])
|
|
344
|
+
asset = %w[.mpk .zip].filter_map do |extension|
|
|
345
|
+
assets.find { _1['name'].to_s.downcase.end_with?(extension) }
|
|
346
|
+
end.first
|
|
347
|
+
asset ? asset.fetch('browser_download_url') : release.fetch('zipball_url')
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def repository_and_name(identifier)
|
|
351
|
+
source = identifier.to_s.delete_prefix('github:')
|
|
352
|
+
repository = source.include?('/') ? source : CATALOG[source]
|
|
353
|
+
raise MarketplaceError, "no GitHub repository is known for #{identifier.inspect}" unless repository
|
|
354
|
+
|
|
355
|
+
[repository, source.split('/').last]
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Safely extracts GitHub or local MPK archives into the target project.
|
|
360
|
+
class Installer # rubocop:disable Metrics/ClassLength
|
|
361
|
+
ATLAS_VARIABLES_IMPORT = '@import "../../themesource/atlas_core/web/variables";'
|
|
362
|
+
IMPORTABLE_CONTENT_TYPES = %w[Module Service].freeze
|
|
363
|
+
DOWNLOADABLE_CONTENT_TYPES = (IMPORTABLE_CONTENT_TYPES + %w[Widget]).freeze
|
|
364
|
+
|
|
365
|
+
def initialize(target:, client: HttpClient.new, mpr: nil)
|
|
366
|
+
@target = File.expand_path(target)
|
|
367
|
+
@client = client
|
|
368
|
+
@mpr = mpr && File.expand_path(mpr)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def pull(identifier, version: nil, mpr: @mpr)
|
|
372
|
+
package = GitHubResolver.new(client: @client).resolve(identifier, version:)
|
|
373
|
+
Dir.mktmpdir('mxrb-marketplace-') do |dir|
|
|
374
|
+
archive = @client.download(package.download_url, File.join(dir, 'package.zip'))
|
|
375
|
+
mpr ? import_module(archive, package, mpr) : install_archive(archive, package)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def pull_official(identifier, version: nil, mendix_version: nil,
|
|
380
|
+
allow_vulnerable: false, api: ContentApi.new)
|
|
381
|
+
mpr = @mpr
|
|
382
|
+
raise MarketplaceError, 'official Marketplace module pull requires --mpr FILE.mpr' unless mpr
|
|
383
|
+
|
|
384
|
+
mendix_version ||= detect_mendix_version(mpr)
|
|
385
|
+
package = api.resolve(identifier, version:, mendix_version:, allow_vulnerable:)
|
|
386
|
+
validate_official_package!(package)
|
|
387
|
+
download_official(package, mpr, api)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
|
|
391
|
+
def update_official(identifier, version: nil, mendix_version: nil,
|
|
392
|
+
allow_vulnerable: false, apply: false, api: ContentApi.new)
|
|
393
|
+
lifecycle = Lifecycle.new(target: @target, mpr: @mpr, installer: self)
|
|
394
|
+
_name, installed = lifecycle.installed(identifier)
|
|
395
|
+
content_id = installed['content_id'] || identifier
|
|
396
|
+
mpr = lifecycle.mpr_path(installed)
|
|
397
|
+
mendix_version ||= detect_mendix_version(mpr)
|
|
398
|
+
package = api.resolve(content_id, version:, mendix_version:, allow_vulnerable:)
|
|
399
|
+
validate_official_package!(package)
|
|
400
|
+
Dir.mktmpdir('mxrb-marketplace-update-') do |dir|
|
|
401
|
+
archive = api.download(
|
|
402
|
+
package.version_id, File.join(dir, 'package.mpk'), download_url: package.download_url
|
|
403
|
+
)
|
|
404
|
+
plan = lifecycle.plan_update(identifier, archive, package)
|
|
405
|
+
plan.apply! if apply
|
|
406
|
+
plan
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
|
|
410
|
+
|
|
411
|
+
def remove(identifier, apply: false)
|
|
412
|
+
plan = Lifecycle.new(target: @target, mpr: @mpr, installer: self).plan_remove(identifier)
|
|
413
|
+
plan.apply! if apply
|
|
414
|
+
plan
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def dependencies(identifier, apply: false, apply_resolved: false, api: ContentApi.new)
|
|
418
|
+
lifecycle = Lifecycle.new(target: @target, mpr: @mpr, installer: self)
|
|
419
|
+
_name, entry = lifecycle.installed(identifier)
|
|
420
|
+
mpr = lifecycle.mpr_path(entry)
|
|
421
|
+
resolver = DependencyResolver.new(
|
|
422
|
+
target: @target, mpr:, installer: self, api:,
|
|
423
|
+
mendix_version: detect_mendix_version(mpr)
|
|
424
|
+
)
|
|
425
|
+
resolver.resolve(identifier, apply:, apply_resolved:)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def download_official(package, mpr, api)
|
|
429
|
+
Dir.mktmpdir('mxrb-marketplace-') do |dir|
|
|
430
|
+
archive = api.download(
|
|
431
|
+
package.version_id, File.join(dir, 'package.mpk'), download_url: package.download_url
|
|
432
|
+
)
|
|
433
|
+
install_official_archive(archive, package, mpr)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def validate_official_package!(package)
|
|
438
|
+
return if DOWNLOADABLE_CONTENT_TYPES.include?(package.content_type)
|
|
439
|
+
|
|
440
|
+
raise MarketplaceError,
|
|
441
|
+
"Marketplace content #{package.name.inspect} is #{package.content_type}, not a Module, Service, or Widget"
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def import(path, name: nil, mpr: @mpr)
|
|
445
|
+
source = File.expand_path(path)
|
|
446
|
+
raise MarketplaceError, "package not found: #{source}" unless File.file?(source)
|
|
447
|
+
|
|
448
|
+
package_name = valid_name(name || File.basename(source, File.extname(source)))
|
|
449
|
+
package = Package.new(package_name, 'local', :mpk, source, nil)
|
|
450
|
+
mpr ? import_module(source, package, mpr) : install_archive(source, package)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
private
|
|
454
|
+
|
|
455
|
+
def install_official_archive(archive, package, mpr) # rubocop:disable Metrics/MethodLength
|
|
456
|
+
case PackageEnvelope.kind(archive)
|
|
457
|
+
when :module
|
|
458
|
+
import_module(archive, package, mpr)
|
|
459
|
+
when :widget
|
|
460
|
+
unless package.content_type == 'Widget'
|
|
461
|
+
raise MarketplaceError,
|
|
462
|
+
"Marketplace #{package.content_type} archive declares a clientModule widget"
|
|
463
|
+
end
|
|
464
|
+
WidgetPackageInstaller.new(target: @target).install(archive, package)
|
|
465
|
+
else
|
|
466
|
+
raise MarketplaceError, 'unsupported official Marketplace package envelope'
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def detect_mendix_version(mpr)
|
|
471
|
+
file = IO::MprFile.open(mpr, readonly: true)
|
|
472
|
+
file.mendix_version
|
|
473
|
+
ensure
|
|
474
|
+
file&.close
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
478
|
+
def import_module(archive, package, mpr)
|
|
479
|
+
relative_target_path(mpr)
|
|
480
|
+
inventory = ModulePackageInventory.read(archive)
|
|
481
|
+
originals, created_backups = preserve_asset_originals(inventory)
|
|
482
|
+
result = module_importer(
|
|
483
|
+
archive, mpr, package, protected_files: protected_package_assets(inventory.name)
|
|
484
|
+
).import!
|
|
485
|
+
ensure_atlas_theme_variables
|
|
486
|
+
resolved = resolved_package(package, result)
|
|
487
|
+
sha256 = Digest::SHA256.file(archive).hexdigest
|
|
488
|
+
cached = cache_package(archive, resolved, sha256)
|
|
489
|
+
write_module_lock(resolved, mpr, result, cached, sha256, originals)
|
|
490
|
+
ModuleInstallation.new(
|
|
491
|
+
resolved, File.expand_path(mpr), sha256, result.module_name, result.module_id,
|
|
492
|
+
result.units, result.files, cached
|
|
493
|
+
)
|
|
494
|
+
rescue StandardError
|
|
495
|
+
Array(created_backups).each { FileUtils.rm_f(_1) }
|
|
496
|
+
raise
|
|
497
|
+
end
|
|
498
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
499
|
+
|
|
500
|
+
def module_importer(archive, mpr, package, protected_files: [])
|
|
501
|
+
ModulePackageImporter.new(
|
|
502
|
+
archive, mpr, target_root: @target, allow_model_upgrade: package.source == :mendix,
|
|
503
|
+
protected_files:
|
|
504
|
+
)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def protected_package_assets(package_name)
|
|
508
|
+
OfficialMarketplace.lock(@target).fetch('packages').flat_map do |name, entry|
|
|
509
|
+
next [] if name == package_name
|
|
510
|
+
|
|
511
|
+
Array(entry['files'])
|
|
512
|
+
end.uniq
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def ensure_atlas_theme_variables
|
|
516
|
+
atlas = File.join(@target, 'themesource', 'atlas_core', 'web', '_variables.scss')
|
|
517
|
+
return unless File.file?(atlas)
|
|
518
|
+
|
|
519
|
+
custom = File.join(@target, 'theme', 'web', 'custom-variables.scss')
|
|
520
|
+
content = File.file?(custom) ? File.read(custom) : ''
|
|
521
|
+
return if content.include?(ATLAS_VARIABLES_IMPORT)
|
|
522
|
+
|
|
523
|
+
FileUtils.mkdir_p(File.dirname(custom))
|
|
524
|
+
File.write(custom, [content.rstrip, ATLAS_VARIABLES_IMPORT, ''].reject(&:empty?).join("\n\n"))
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def resolved_package(package, result)
|
|
528
|
+
manifest_version = result.package_version.to_s
|
|
529
|
+
version = manifest_version.empty? || manifest_version == 'unknown' ? package.version : manifest_version
|
|
530
|
+
values = { name: result.module_name, version: }
|
|
531
|
+
return package.with(**values) if package.respond_to?(:content_id)
|
|
532
|
+
|
|
533
|
+
Package.new(
|
|
534
|
+
values.fetch(:name), values.fetch(:version), package.source,
|
|
535
|
+
package.download_url, package.repository
|
|
536
|
+
)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def cache_package(archive, package, sha256)
|
|
540
|
+
version = package.version.to_s.gsub(/[^A-Za-z0-9_.-]/, '_')
|
|
541
|
+
destination = File.join(@target, '.mxrb', 'marketplace', "#{package.name}-#{version}.mpk")
|
|
542
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
543
|
+
FileUtils.cp(archive, destination) unless File.expand_path(archive) == destination
|
|
544
|
+
raise MarketplaceError, 'cached marketplace package checksum mismatch' unless
|
|
545
|
+
Digest::SHA256.file(destination).hexdigest == sha256
|
|
546
|
+
|
|
547
|
+
destination
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
|
|
551
|
+
def write_module_lock(package, mpr, result, cached, digest, originals)
|
|
552
|
+
path = File.join(@target, '.mxrb', 'marketplace.lock.json')
|
|
553
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
554
|
+
lock = OfficialMarketplace.lock(@target)
|
|
555
|
+
lock['packages'][package.name] = {
|
|
556
|
+
'kind' => 'module', 'version' => package.version, 'source' => package.source,
|
|
557
|
+
'repository' => package.repository, 'sha256' => digest,
|
|
558
|
+
'destination' => relative_target_path(mpr), 'archive' => relative_target_path(cached),
|
|
559
|
+
'module_id' => result.module_id, 'units' => result.units, 'files' => result.files,
|
|
560
|
+
'asset_originals' => originals
|
|
561
|
+
}.merge(official_lock_metadata(package)).compact
|
|
562
|
+
write_lock_atomically(path, lock)
|
|
563
|
+
end
|
|
564
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
|
|
565
|
+
|
|
566
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
567
|
+
def preserve_asset_originals(inventory)
|
|
568
|
+
existing = OfficialMarketplace.lock(@target).dig('packages', inventory.name)
|
|
569
|
+
legacy = Array(existing&.fetch('files', [])).to_h { [_1, nil] }
|
|
570
|
+
retained = existing&.fetch('asset_originals', legacy) || {}
|
|
571
|
+
originals = inventory.files.keys.to_h do |relative|
|
|
572
|
+
[relative, retained.key?(relative) ? retained[relative] : preserve_asset(relative, inventory.name)]
|
|
573
|
+
end
|
|
574
|
+
created = originals.values.compact.filter_map do |relative|
|
|
575
|
+
path = safe_target_path(relative)
|
|
576
|
+
path unless retained.value?(relative)
|
|
577
|
+
end
|
|
578
|
+
[originals, created]
|
|
579
|
+
end
|
|
580
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
581
|
+
|
|
582
|
+
def preserve_asset(relative, module_name)
|
|
583
|
+
source = safe_target_path(relative)
|
|
584
|
+
raise MarketplaceError, "package asset destination is a symbolic link: #{relative}" if File.symlink?(source)
|
|
585
|
+
return unless File.file?(source)
|
|
586
|
+
|
|
587
|
+
backup = safe_target_path(File.join('.mxrb', 'marketplace-originals', valid_name(module_name), relative))
|
|
588
|
+
FileUtils.mkdir_p(File.dirname(backup))
|
|
589
|
+
FileUtils.cp(source, backup)
|
|
590
|
+
relative_target_path(backup)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def safe_target_path(relative)
|
|
594
|
+
path = File.expand_path(File.join(@target, relative.to_s))
|
|
595
|
+
prefix = "#{@target}#{File::SEPARATOR}"
|
|
596
|
+
raise MarketplaceError, "path is outside marketplace target: #{path}" unless path.start_with?(prefix)
|
|
597
|
+
|
|
598
|
+
path
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def official_lock_metadata(package)
|
|
602
|
+
return {} unless package.respond_to?(:content_id)
|
|
603
|
+
|
|
604
|
+
{
|
|
605
|
+
'content_id' => package.content_id, 'version_id' => package.version_id,
|
|
606
|
+
'content_type' => package.content_type, 'version_type' => package.version_type,
|
|
607
|
+
'security_issues' => package.security_issues, 'private' => package.private,
|
|
608
|
+
'company_approved' => package.company_approved
|
|
609
|
+
}
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def relative_target_path(path)
|
|
613
|
+
absolute = File.expand_path(path)
|
|
614
|
+
prefix = "#{@target}#{File::SEPARATOR}"
|
|
615
|
+
raise MarketplaceError, "path is outside marketplace target: #{absolute}" unless absolute.start_with?(prefix)
|
|
616
|
+
|
|
617
|
+
absolute.delete_prefix(prefix)
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# Transaction boundary intentionally keeps rollback and cleanup together.
|
|
621
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
622
|
+
def install_archive(archive, package)
|
|
623
|
+
name = valid_name(package.name)
|
|
624
|
+
destination = File.join(@target, 'modules', name)
|
|
625
|
+
raise MarketplaceError, "module destination already exists: #{destination}" if File.exist?(destination)
|
|
626
|
+
|
|
627
|
+
staging = File.join(@target, '.mxrb', 'marketplace-staging', "#{name}-#{Process.pid}")
|
|
628
|
+
extract_zip(archive, staging)
|
|
629
|
+
move_into_project(staging, destination)
|
|
630
|
+
digest = OfficialMarketplace.tree_digest(destination)
|
|
631
|
+
write_lock(package, destination, digest)
|
|
632
|
+
Installation.new(package, destination, digest)
|
|
633
|
+
rescue StandardError
|
|
634
|
+
FileUtils.rm_rf(destination) if defined?(destination) && destination && File.exist?(destination)
|
|
635
|
+
raise
|
|
636
|
+
ensure
|
|
637
|
+
FileUtils.rm_rf(staging) if defined?(staging) && staging && File.exist?(staging)
|
|
638
|
+
end
|
|
639
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
640
|
+
|
|
641
|
+
def extract_zip(archive, staging)
|
|
642
|
+
FileUtils.mkdir_p(staging)
|
|
643
|
+
Zip::File.open(archive) do |zip|
|
|
644
|
+
zip.each { extract_safe_entry(_1, staging) }
|
|
645
|
+
end
|
|
646
|
+
rescue Zip::Error => e
|
|
647
|
+
raise MarketplaceError, "invalid MPK/ZIP package: #{e.message}"
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def extract_safe_entry(entry, staging)
|
|
651
|
+
destination = safe_destination(entry.name, staging)
|
|
652
|
+
|
|
653
|
+
entry.directory? ? FileUtils.mkdir_p(destination) : extract_entry(entry, destination)
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def safe_destination(name, staging)
|
|
657
|
+
relative = name.to_s
|
|
658
|
+
unsafe = relative.start_with?('/', '\\') || relative.split(%r{[\\/]}).include?('..')
|
|
659
|
+
raise MarketplaceError, "unsafe package path #{name.inspect}" if unsafe
|
|
660
|
+
|
|
661
|
+
destination = File.expand_path(File.join(staging, relative))
|
|
662
|
+
prefix = "#{File.expand_path(staging)}#{File::SEPARATOR}"
|
|
663
|
+
raise MarketplaceError, "unsafe package path #{name.inspect}" unless destination.start_with?(prefix)
|
|
664
|
+
|
|
665
|
+
destination
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def extract_entry(entry, destination)
|
|
669
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
670
|
+
File.open(destination, 'wb') { ::IO.copy_stream(entry.get_input_stream, _1) }
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def collapse_single_root(staging)
|
|
674
|
+
children = Dir.children(staging)
|
|
675
|
+
if children.one? && File.directory?(File.join(staging, children.first))
|
|
676
|
+
return File.join(staging, children.first)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
staging
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def move_into_project(staging, destination)
|
|
683
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
684
|
+
FileUtils.mv(collapse_single_root(staging), destination)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def valid_name(value)
|
|
688
|
+
name = value.to_s.gsub(/[^A-Za-z0-9_]/, '')
|
|
689
|
+
raise MarketplaceError, "invalid marketplace module name #{value.inspect}" \
|
|
690
|
+
unless name.match?(/\A[A-Za-z][A-Za-z0-9_]*\z/)
|
|
691
|
+
|
|
692
|
+
name
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def write_lock(package, destination, digest)
|
|
696
|
+
path = File.join(@target, '.mxrb', 'marketplace.lock.json')
|
|
697
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
698
|
+
lock = OfficialMarketplace.lock(@target)
|
|
699
|
+
lock['packages'][package.name] = lock_entry(package, destination, digest)
|
|
700
|
+
write_lock_atomically(path, lock)
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def lock_entry(package, destination, digest)
|
|
704
|
+
{
|
|
705
|
+
'version' => package.version, 'source' => package.source,
|
|
706
|
+
'repository' => package.repository, 'sha256' => digest,
|
|
707
|
+
'destination' => destination.delete_prefix("#{@target}/")
|
|
708
|
+
}.merge(official_lock_metadata(package)).compact
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def write_lock_atomically(path, lock)
|
|
712
|
+
temporary = "#{path}.tmp-#{Process.pid}"
|
|
713
|
+
File.write(temporary, JSON.pretty_generate(lock) << "\n")
|
|
714
|
+
File.rename(temporary, path)
|
|
715
|
+
ensure
|
|
716
|
+
FileUtils.rm_f(temporary) if defined?(temporary) && temporary && File.exist?(temporary)
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
end
|