ibex 0.1.0 → 0.2.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 +4 -4
- data/README.md +247 -104
- data/docs/architecture.md +322 -28
- data/docs/cst-migration.md +107 -0
- data/docs/cst.md +213 -0
- data/docs/development.md +129 -0
- data/docs/editor-setup.md +25 -0
- data/docs/error-ux.md +60 -0
- data/docs/grammar-reference.md +458 -18
- data/docs/lexer-migration.md +51 -0
- data/docs/racc-migration.md +34 -6
- data/docs/release-readiness.md +163 -0
- data/docs/stability.md +124 -0
- data/examples/README.md +60 -0
- data/examples/calculator.y +44 -0
- data/examples/csv.y +32 -0
- data/examples/ini.y +70 -0
- data/examples/json.y +48 -0
- data/examples/tiny_language.y +75 -0
- data/lib/ibex/analysis/sets.rb +5 -4
- data/lib/ibex/artifact_set.rb +63 -0
- data/lib/ibex/cli/ambiguity.rb +66 -0
- data/lib/ibex/cli/counterexample_options.rb +6 -2
- data/lib/ibex/cli/coverage.rb +173 -0
- data/lib/ibex/cli/debug.rb +106 -0
- data/lib/ibex/cli/diagnostics.rb +116 -0
- data/lib/ibex/cli/documentation.rb +67 -0
- data/lib/ibex/cli/error_messages.rb +177 -0
- data/lib/ibex/cli/explain.rb +76 -0
- data/lib/ibex/cli/formatting.rb +386 -0
- data/lib/ibex/cli/generation_artifacts.rb +138 -0
- data/lib/ibex/cli/generation_error_messages.rb +39 -0
- data/lib/ibex/cli/grammar_tests.rb +122 -0
- data/lib/ibex/cli/ir_tools.rb +184 -0
- data/lib/ibex/cli/lsp.rb +33 -0
- data/lib/ibex/cli/outputs.rb +91 -8
- data/lib/ibex/cli/racc_migration.rb +87 -0
- data/lib/ibex/cli/samples.rb +92 -0
- data/lib/ibex/cli/watch.rb +103 -0
- data/lib/ibex/cli.rb +506 -37
- data/lib/ibex/codegen/action_locations.rb +103 -0
- data/lib/ibex/codegen/action_method_source.rb +210 -0
- data/lib/ibex/codegen/action_source.rb +118 -0
- data/lib/ibex/codegen/ambiguity.rb +173 -0
- data/lib/ibex/codegen/cst_metadata.rb +171 -0
- data/lib/ibex/codegen/documentation.rb +170 -0
- data/lib/ibex/codegen/explain.rb +312 -0
- data/lib/ibex/codegen/generated_action_abi.rb +288 -0
- data/lib/ibex/codegen/html.rb +94 -10
- data/lib/ibex/codegen/mermaid.rb +43 -0
- data/lib/ibex/codegen/railroad.rb +197 -0
- data/lib/ibex/codegen/railroad_documentation.rb +59 -0
- data/lib/ibex/codegen/rbs.rb +323 -2
- data/lib/ibex/codegen/report.rb +46 -6
- data/lib/ibex/codegen/ruby.rb +334 -59
- data/lib/ibex/codegen/ruby_actions.rb +143 -0
- data/lib/ibex/codegen/ruby_ast.rb +121 -0
- data/lib/ibex/codegen/ruby_error_messages.rb +28 -0
- data/lib/ibex/codegen/ruby_lexer.rb +83 -0
- data/lib/ibex/codegen/ruby_syntax.rb +90 -0
- data/lib/ibex/codegen/ruby_table_metadata.rb +57 -0
- data/lib/ibex/codegen/ruby_value_printers.rb +56 -0
- data/lib/ibex/codegen/symbol_labels.rb +1 -1
- data/lib/ibex/coverage/collector.rb +188 -0
- data/lib/ibex/coverage/event_stream.rb +97 -0
- data/lib/ibex/coverage/report.rb +259 -0
- data/lib/ibex/coverage/runtime_event_validator.rb +160 -0
- data/lib/ibex/coverage.rb +13 -0
- data/lib/ibex/error_messages/parser.rb +159 -0
- data/lib/ibex/error_messages/parser_v2.rb +198 -0
- data/lib/ibex/error_messages/renderer.rb +65 -0
- data/lib/ibex/error_messages/sentence_search.rb +196 -0
- data/lib/ibex/error_messages/update.rb +169 -0
- data/lib/ibex/error_messages.rb +165 -0
- data/lib/ibex/frontend/ast.rb +145 -6
- data/lib/ibex/frontend/bootstrap_parser.rb +47 -4
- data/lib/ibex/frontend/diagnostic.rb +81 -0
- data/lib/ibex/frontend/diagnostic_recovery.rb +268 -0
- data/lib/ibex/frontend/dsl.rb +66 -7
- data/lib/ibex/frontend/formatter.rb +407 -0
- data/lib/ibex/frontend/generated_parser.rb +614 -190
- data/lib/ibex/frontend/generated_parser_base.rb +164 -30
- data/lib/ibex/frontend/generated_parser_includes.rb +61 -0
- data/lib/ibex/frontend/generated_parser_metadata.rb +61 -0
- data/lib/ibex/frontend/generated_parser_parameters.rb +60 -0
- data/lib/ibex/frontend/generation.rb +33 -0
- data/lib/ibex/frontend/lexer.rb +162 -10
- data/lib/ibex/frontend/lexer_recovery.rb +84 -0
- data/lib/ibex/frontend/parser/declarations.rb +247 -11
- data/lib/ibex/frontend/parser/parameters.rb +82 -0
- data/lib/ibex/frontend/parser/rules.rb +43 -6
- data/lib/ibex/frontend/parser.rb +182 -4
- data/lib/ibex/frontend/regenerator.rb +26 -1
- data/lib/ibex/frontend/resolution.rb +69 -0
- data/lib/ibex/frontend/resolver.rb +217 -0
- data/lib/ibex/frontend/rule_documentation.rb +103 -0
- data/lib/ibex/frontend/source_cursor.rb +132 -8
- data/lib/ibex/frontend/source_document.rb +229 -0
- data/lib/ibex/frontend/source_loader.rb +150 -0
- data/lib/ibex/frontend/source_span.rb +81 -0
- data/lib/ibex/frontend/token_adapter/declaration_document_state.rb +47 -0
- data/lib/ibex/frontend/token_adapter/declaration_lexer_state.rb +83 -0
- data/lib/ibex/frontend/token_adapter/declaration_state.rb +216 -26
- data/lib/ibex/frontend/token_adapter/delimiter_tracker.rb +8 -2
- data/lib/ibex/frontend/token_adapter/rule_state.rb +60 -2
- data/lib/ibex/frontend/token_adapter.rb +8 -3
- data/lib/ibex/frontend.rb +13 -2
- data/lib/ibex/generation_input.rb +57 -0
- data/lib/ibex/generation_manifest.rb +200 -0
- data/lib/ibex/generation_transaction.rb +261 -0
- data/lib/ibex/generation_transaction_recovery.rb +109 -0
- data/lib/ibex/generation_transaction_validation.rb +196 -0
- data/lib/ibex/grammar_tests.rb +206 -0
- data/lib/ibex/ir/automaton_ir.rb +38 -5
- data/lib/ibex/ir/grammar_ir.rb +137 -24
- data/lib/ibex/ir/lexer_ir.rb +76 -0
- data/lib/ibex/ir/migration.rb +120 -0
- data/lib/ibex/ir/serialize.rb +110 -19
- data/lib/ibex/ir/validator/automaton.rb +345 -0
- data/lib/ibex/ir/validator/base.rb +129 -0
- data/lib/ibex/ir/validator/grammar.rb +604 -0
- data/lib/ibex/ir/validator/lexer.rb +113 -0
- data/lib/ibex/ir/validator.rb +62 -0
- data/lib/ibex/ir.rb +57 -4
- data/lib/ibex/lalr/build_metrics.rb +23 -0
- data/lib/ibex/lalr/builder.rb +379 -52
- data/lib/ibex/lalr/conflict.rb +1 -0
- data/lib/ibex/lalr/conflict_search.rb +11 -5
- data/lib/ibex/lalr/counterexample.rb +20 -5
- data/lib/ibex/lalr/direct_lookaheads.rb +236 -0
- data/lib/ibex/lalr/ielr_partition.rb +152 -0
- data/lib/ibex/lalr/on_error_reductions.rb +74 -0
- data/lib/ibex/lalr.rb +7 -0
- data/lib/ibex/location.rb +129 -0
- data/lib/ibex/lsp/document_handlers.rb +66 -0
- data/lib/ibex/lsp/document_store.rb +264 -0
- data/lib/ibex/lsp/document_store_diagnostics.rb +44 -0
- data/lib/ibex/lsp/document_store_validation.rb +61 -0
- data/lib/ibex/lsp/initialization_handlers.rb +78 -0
- data/lib/ibex/lsp/navigation_handlers.rb +56 -0
- data/lib/ibex/lsp/position_codec.rb +109 -0
- data/lib/ibex/lsp/protocol_error.rb +33 -0
- data/lib/ibex/lsp/request_handlers.rb +45 -0
- data/lib/ibex/lsp/request_support.rb +87 -0
- data/lib/ibex/lsp/server.rb +146 -0
- data/lib/ibex/lsp/symbol_index.rb +241 -0
- data/lib/ibex/lsp/symbol_index_builder.rb +267 -0
- data/lib/ibex/lsp/symbol_index_precedence_references.rb +44 -0
- data/lib/ibex/lsp/symbol_index_source_queries.rb +61 -0
- data/lib/ibex/lsp/symbol_occurrence.rb +17 -0
- data/lib/ibex/lsp/transport.rb +118 -0
- data/lib/ibex/lsp/workspace.rb +127 -0
- data/lib/ibex/lsp/workspace_analyzer.rb +199 -0
- data/lib/ibex/lsp.rb +32 -0
- data/lib/ibex/normalize/declarations.rb +140 -7
- data/lib/ibex/normalize/diagnostics.rb +50 -5
- data/lib/ibex/normalize/expander.rb +59 -55
- data/lib/ibex/normalize/expression.rb +60 -39
- data/lib/ibex/normalize/inline_expansion.rb +414 -0
- data/lib/ibex/normalize/inline_validation.rb +174 -0
- data/lib/ibex/normalize/lexer.rb +131 -0
- data/lib/ibex/normalize/named_references.rb +60 -0
- data/lib/ibex/normalize/nodes.rb +46 -0
- data/lib/ibex/normalize/parameter_ebnf_lowering.rb +69 -0
- data/lib/ibex/normalize/parameter_lowering.rb +126 -0
- data/lib/ibex/normalize/parameter_substitution.rb +125 -0
- data/lib/ibex/normalize/parameter_validation.rb +140 -0
- data/lib/ibex/normalize/parameters.rb +199 -0
- data/lib/ibex/normalize/recovery_declarations.rb +84 -0
- data/lib/ibex/normalize.rb +179 -14
- data/lib/ibex/racc_migration/checker.rb +122 -0
- data/lib/ibex/racc_migration/harness.rb +177 -0
- data/lib/ibex/racc_migration/report.rb +94 -0
- data/lib/ibex/racc_migration.rb +12 -0
- data/lib/ibex/rake_task.rb +116 -0
- data/lib/ibex/samples.rb +186 -0
- data/lib/ibex/table_simulation/result.rb +51 -0
- data/lib/ibex/table_simulation/simulator.rb +253 -0
- data/lib/ibex/table_simulation/step.rb +60 -0
- data/lib/ibex/table_simulation/text.rb +31 -0
- data/lib/ibex/table_simulation.rb +13 -0
- data/lib/ibex/tables.rb +9 -70
- data/lib/ibex/version.rb +1 -1
- data/lib/ibex/watch/runner.rb +172 -0
- data/lib/ibex/watch/source_snapshot.rb +93 -0
- data/lib/ibex/watch.rb +11 -0
- data/lib/ibex.rb +25 -1
- data/schema/automaton-ir-v1.schema.json +401 -0
- data/schema/automaton-ir-v2.schema.json +58 -0
- data/schema/benchmark-v1.schema.json +212 -0
- data/schema/benchmark-v2.schema.json +61 -0
- data/schema/cst-v1.json +170 -0
- data/schema/error-ux-v1.schema.json +258 -0
- data/schema/explain-v1.schema.json +433 -0
- data/schema/frontend-diagnostics-v1.schema.json +154 -0
- data/schema/generation-manifest-v1.schema.json +115 -0
- data/schema/grammar-ir-v1.schema.json +426 -0
- data/schema/grammar-ir-v2.schema.json +779 -0
- data/schema/lexer-ir-v1.schema.json +215 -0
- data/schema/migration-check-v1.schema.json +60 -0
- data/schema/performance-comparison-v1.schema.json +395 -0
- data/schema/public-performance-comparison-v1.schema.json +506 -0
- data/schema/public-performance-profile-v1.schema.json +360 -0
- data/schema/runtime-coverage-v1.schema.json +86 -0
- data/schema/runtime-event-v1.schema.json +308 -0
- data/schema/table-simulation-v1.schema.json +70 -0
- data/sig/ibex/artifact_set.rbs +37 -0
- data/sig/ibex/cli/ambiguity.rbs +22 -0
- data/sig/ibex/cli/counterexample_options.rbs +2 -0
- data/sig/ibex/cli/coverage.rbs +53 -0
- data/sig/ibex/cli/debug.rbs +28 -0
- data/sig/ibex/cli/diagnostics.rbs +38 -0
- data/sig/ibex/cli/documentation.rbs +25 -0
- data/sig/ibex/cli/error_messages.rbs +57 -0
- data/sig/ibex/cli/explain.rbs +25 -0
- data/sig/ibex/cli/formatting.rbs +103 -0
- data/sig/ibex/cli/generation_artifacts.rbs +50 -0
- data/sig/ibex/cli/generation_error_messages.rbs +19 -0
- data/sig/ibex/cli/grammar_tests.rbs +36 -0
- data/sig/ibex/cli/ir_tools.rbs +51 -0
- data/sig/ibex/cli/lsp.rbs +14 -0
- data/sig/ibex/cli/outputs.rbs +17 -0
- data/sig/ibex/cli/racc_migration.rbs +30 -0
- data/sig/ibex/cli/samples.rbs +30 -0
- data/sig/ibex/cli/watch.rbs +43 -0
- data/sig/ibex/cli.rbs +104 -5
- data/sig/ibex/codegen/action_locations.rbs +45 -0
- data/sig/ibex/codegen/action_method_source.rbs +65 -0
- data/sig/ibex/codegen/action_source.rbs +50 -0
- data/sig/ibex/codegen/ambiguity.rbs +60 -0
- data/sig/ibex/codegen/cst_metadata.rbs +59 -0
- data/sig/ibex/codegen/documentation.rbs +50 -0
- data/sig/ibex/codegen/explain.rbs +85 -0
- data/sig/ibex/codegen/generated_action_abi.rbs +101 -0
- data/sig/ibex/codegen/html.rbs +18 -2
- data/sig/ibex/codegen/mermaid.rbs +16 -0
- data/sig/ibex/codegen/railroad.rbs +82 -0
- data/sig/ibex/codegen/railroad_documentation.rbs +31 -0
- data/sig/ibex/codegen/rbs.rbs +84 -4
- data/sig/ibex/codegen/report.rbs +8 -0
- data/sig/ibex/codegen/ruby.rbs +97 -23
- data/sig/ibex/codegen/ruby_actions.rbs +54 -0
- data/sig/ibex/codegen/ruby_ast.rbs +34 -0
- data/sig/ibex/codegen/ruby_error_messages.rbs +16 -0
- data/sig/ibex/codegen/ruby_lexer.rbs +25 -0
- data/sig/ibex/codegen/ruby_syntax.rbs +25 -0
- data/sig/ibex/codegen/ruby_table_metadata.rbs +26 -0
- data/sig/ibex/codegen/ruby_value_printers.rbs +28 -0
- data/sig/ibex/coverage/collector.rbs +76 -0
- data/sig/ibex/coverage/event_stream.rbs +42 -0
- data/sig/ibex/coverage/report.rbs +100 -0
- data/sig/ibex/coverage/runtime_event_validator.rbs +68 -0
- data/sig/ibex/coverage.rbs +7 -0
- data/sig/ibex/error_messages/parser.rbs +58 -0
- data/sig/ibex/error_messages/parser_v2.rbs +67 -0
- data/sig/ibex/error_messages/renderer.rbs +23 -0
- data/sig/ibex/error_messages/sentence_search.rbs +80 -0
- data/sig/ibex/error_messages/update.rbs +43 -0
- data/sig/ibex/error_messages.rbs +85 -0
- data/sig/ibex/frontend/ast.rbs +208 -19
- data/sig/ibex/frontend/bootstrap_parser.rbs +11 -0
- data/sig/ibex/frontend/diagnostic.rbs +53 -0
- data/sig/ibex/frontend/diagnostic_recovery.rbs +98 -0
- data/sig/ibex/frontend/dsl.rbs +33 -4
- data/sig/ibex/frontend/formatter.rbs +135 -0
- data/sig/ibex/frontend/generated_parser.rbs +218 -68
- data/sig/ibex/frontend/generated_parser_base.rbs +61 -10
- data/sig/ibex/frontend/generated_parser_includes.rbs +23 -0
- data/sig/ibex/frontend/generated_parser_metadata.rbs +23 -0
- data/sig/ibex/frontend/generated_parser_parameters.rbs +24 -0
- data/sig/ibex/frontend/generation.rbs +6 -0
- data/sig/ibex/frontend/lexer.rbs +49 -2
- data/sig/ibex/frontend/lexer_recovery.rbs +25 -0
- data/sig/ibex/frontend/parser/declarations.rbs +54 -0
- data/sig/ibex/frontend/parser/parameters.rbs +28 -0
- data/sig/ibex/frontend/parser/rules.rbs +3 -0
- data/sig/ibex/frontend/parser.rbs +66 -0
- data/sig/ibex/frontend/regenerator.rbs +11 -0
- data/sig/ibex/frontend/resolution.rbs +33 -0
- data/sig/ibex/frontend/resolver.rbs +91 -0
- data/sig/ibex/frontend/rule_documentation.rbs +42 -0
- data/sig/ibex/frontend/source_cursor.rbs +36 -3
- data/sig/ibex/frontend/source_document.rbs +119 -0
- data/sig/ibex/frontend/source_loader.rbs +66 -0
- data/sig/ibex/frontend/source_span.rbs +53 -0
- data/sig/ibex/frontend/token_adapter/declaration_document_state.rbs +21 -0
- data/sig/ibex/frontend/token_adapter/declaration_lexer_state.rbs +27 -0
- data/sig/ibex/frontend/token_adapter/declaration_state.rbs +73 -5
- data/sig/ibex/frontend/token_adapter/rule_state.rbs +20 -0
- data/sig/ibex/frontend/token_adapter.rbs +5 -2
- data/sig/ibex/frontend.rbs +1 -1
- data/sig/ibex/generation_input.rbs +37 -0
- data/sig/ibex/generation_manifest.rbs +67 -0
- data/sig/ibex/generation_transaction.rbs +82 -0
- data/sig/ibex/generation_transaction_recovery.rbs +36 -0
- data/sig/ibex/generation_transaction_validation.rbs +65 -0
- data/sig/ibex/grammar_tests.rbs +93 -0
- data/sig/ibex/ir/automaton_ir.rbs +8 -2
- data/sig/ibex/ir/grammar_ir.rbs +75 -15
- data/sig/ibex/ir/lexer_ir.rbs +57 -0
- data/sig/ibex/ir/migration.rbs +34 -0
- data/sig/ibex/ir/serialize.rbs +23 -6
- data/sig/ibex/ir/validator/automaton.rbs +109 -0
- data/sig/ibex/ir/validator/base.rbs +65 -0
- data/sig/ibex/ir/validator/grammar.rbs +184 -0
- data/sig/ibex/ir/validator/lexer.rbs +37 -0
- data/sig/ibex/ir/validator.rbs +16 -0
- data/sig/ibex/ir.rbs +38 -4
- data/sig/ibex/lalr/build_metrics.rbs +20 -0
- data/sig/ibex/lalr/builder.rbs +95 -15
- data/sig/ibex/lalr/conflict_search.rbs +7 -3
- data/sig/ibex/lalr/counterexample.rbs +5 -2
- data/sig/ibex/lalr/direct_lookaheads.rbs +86 -0
- data/sig/ibex/lalr/ielr_partition.rbs +59 -0
- data/sig/ibex/lalr/on_error_reductions.rbs +22 -0
- data/sig/ibex/lalr.rbs +6 -0
- data/sig/ibex/location.rbs +67 -0
- data/sig/ibex/lsp/document_handlers.rbs +24 -0
- data/sig/ibex/lsp/document_store.rbs +94 -0
- data/sig/ibex/lsp/document_store_diagnostics.rbs +20 -0
- data/sig/ibex/lsp/document_store_validation.rbs +26 -0
- data/sig/ibex/lsp/initialization_handlers.rbs +30 -0
- data/sig/ibex/lsp/navigation_handlers.rbs +30 -0
- data/sig/ibex/lsp/position_codec.rbs +40 -0
- data/sig/ibex/lsp/protocol_error.rbs +32 -0
- data/sig/ibex/lsp/request_handlers.rbs +26 -0
- data/sig/ibex/lsp/request_support.rbs +40 -0
- data/sig/ibex/lsp/server.rbs +55 -0
- data/sig/ibex/lsp/symbol_index.rbs +78 -0
- data/sig/ibex/lsp/symbol_index_builder.rbs +87 -0
- data/sig/ibex/lsp/symbol_index_precedence_references.rbs +18 -0
- data/sig/ibex/lsp/symbol_index_source_queries.rbs +26 -0
- data/sig/ibex/lsp/symbol_occurrence.rbs +25 -0
- data/sig/ibex/lsp/transport.rbs +35 -0
- data/sig/ibex/lsp/workspace.rbs +41 -0
- data/sig/ibex/lsp/workspace_analyzer.rbs +69 -0
- data/sig/ibex/lsp.rbs +7 -0
- data/sig/ibex/normalize/declarations.rbs +31 -0
- data/sig/ibex/normalize/diagnostics.rbs +9 -0
- data/sig/ibex/normalize/expander.rbs +20 -14
- data/sig/ibex/normalize/expression.rbs +10 -10
- data/sig/ibex/normalize/inline_expansion.rbs +120 -0
- data/sig/ibex/normalize/inline_validation.rbs +40 -0
- data/sig/ibex/normalize/lexer.rbs +37 -0
- data/sig/ibex/normalize/named_references.rbs +20 -0
- data/sig/ibex/normalize/nodes.rbs +14 -0
- data/sig/ibex/normalize/parameter_ebnf_lowering.rbs +32 -0
- data/sig/ibex/normalize/parameter_lowering.rbs +35 -0
- data/sig/ibex/normalize/parameter_substitution.rbs +42 -0
- data/sig/ibex/normalize/parameter_validation.rbs +44 -0
- data/sig/ibex/normalize/parameters.rbs +46 -0
- data/sig/ibex/normalize/recovery_declarations.rbs +24 -0
- data/sig/ibex/normalize.rbs +123 -18
- data/sig/ibex/racc_migration/checker.rbs +36 -0
- data/sig/ibex/racc_migration/harness.rbs +16 -0
- data/sig/ibex/racc_migration/report.rbs +53 -0
- data/sig/ibex/racc_migration.rbs +8 -0
- data/sig/ibex/rake_task.rbs +51 -0
- data/sig/ibex/samples.rbs +51 -0
- data/sig/ibex/table_simulation/result.rbs +32 -0
- data/sig/ibex/table_simulation/simulator.rbs +98 -0
- data/sig/ibex/table_simulation/step.rbs +40 -0
- data/sig/ibex/table_simulation/text.rbs +14 -0
- data/sig/ibex/table_simulation.rbs +7 -0
- data/sig/ibex/tables.rbs +0 -26
- data/sig/ibex/watch/runner.rbs +48 -0
- data/sig/ibex/watch/source_snapshot.rbs +42 -0
- data/sig/ibex/watch.rbs +7 -0
- data/sig/ibex.rbs +2 -0
- metadata +301 -16
- data/.rubocop.yml +0 -43
- data/CHANGELOG.md +0 -30
- data/Rakefile +0 -25
- data/Steepfile +0 -10
- data/docs/compat-notes.md +0 -37
- data/docs/lexer-coverage.md +0 -14
- data/docs/phase10-extensions.md +0 -27
- data/gemfiles/Gemfile +0 -7
- data/gemfiles/Gemfile.lock +0 -98
- data/lib/ibex/frontend/grammar.y +0 -156
- data/lib/ibex/runtime/parser.rb +0 -360
- data/lib/ibex/runtime.rb +0 -8
- data/sig/ibex/runtime/parser.rbs +0 -167
- data/sig/ibex/runtime.rbs +0 -6
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "digest"
|
|
5
|
+
|
|
6
|
+
module Ibex
|
|
7
|
+
# Digest and identity of source bytes actually consumed by one generation.
|
|
8
|
+
class GenerationInput
|
|
9
|
+
attr_reader :path #: String
|
|
10
|
+
attr_reader :sha256 #: String
|
|
11
|
+
attr_reader :bytesize #: Integer
|
|
12
|
+
attr_reader :dev #: Integer
|
|
13
|
+
attr_reader :ino #: Integer
|
|
14
|
+
attr_reader :access_paths #: Array[String]
|
|
15
|
+
|
|
16
|
+
# @rbs (String path, String source, ?access_path: String) -> void
|
|
17
|
+
def initialize(path, source, access_path: path)
|
|
18
|
+
@path = File.realpath(path).freeze
|
|
19
|
+
stat = File.stat(@path)
|
|
20
|
+
@sha256 = Digest::SHA256.hexdigest(source).freeze
|
|
21
|
+
@bytesize = source.bytesize
|
|
22
|
+
@dev = stat.dev
|
|
23
|
+
@ino = stat.ino
|
|
24
|
+
@access_paths = [File.expand_path(access_path)] #: Array[String]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Record another lexical path that supplied these canonical bytes.
|
|
28
|
+
# @rbs (String path) -> void
|
|
29
|
+
def add_access_path(path)
|
|
30
|
+
expanded = File.expand_path(path)
|
|
31
|
+
@access_paths << expanded unless @access_paths.include?(expanded)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @rbs () -> Hash[String, untyped]
|
|
35
|
+
def to_h
|
|
36
|
+
{ "path" => @path, "sha256" => @sha256, "bytesize" => @bytesize }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Detect content changes even when size and timestamps are preserved.
|
|
40
|
+
# @rbs () -> bool
|
|
41
|
+
def current?
|
|
42
|
+
source = File.binread(@path)
|
|
43
|
+
stat = File.stat(@path)
|
|
44
|
+
lexical_paths_current? && stat.dev == @dev && stat.ino == @ino &&
|
|
45
|
+
source.bytesize == @bytesize && Digest::SHA256.hexdigest(source) == @sha256
|
|
46
|
+
rescue SystemCallError
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# @rbs () -> bool
|
|
53
|
+
def lexical_paths_current?
|
|
54
|
+
@access_paths.all? { |path| File.realpath(path) == @path }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "digest"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Ibex
|
|
8
|
+
# Deterministic description of one published parser generation.
|
|
9
|
+
module GenerationManifest
|
|
10
|
+
SCHEMA_VERSION = 1 #: Integer
|
|
11
|
+
IDENTIFIER = "generation" #: String
|
|
12
|
+
ROOT_KEYS = %w[ibex_manifest schema_version input options artifacts].freeze #: Array[String]
|
|
13
|
+
INPUT_KEYS = %w[root sha256 files].freeze #: Array[String]
|
|
14
|
+
FILE_KEYS = %w[path sha256 bytesize].freeze #: Array[String]
|
|
15
|
+
ARTIFACT_KEYS = %w[kind path sha256 bytesize].freeze #: Array[String]
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# @rbs (ArtifactSet artifacts, source_records: Array[GenerationInput], options: Hash[String, untyped]) -> String
|
|
20
|
+
def render(artifacts, source_records:, options:)
|
|
21
|
+
files = source_records.map(&:to_h)
|
|
22
|
+
document = {
|
|
23
|
+
"ibex_manifest" => IDENTIFIER,
|
|
24
|
+
"schema_version" => SCHEMA_VERSION,
|
|
25
|
+
"input" => {
|
|
26
|
+
"root" => files.fetch(0).fetch("path"),
|
|
27
|
+
"sha256" => input_digest(files),
|
|
28
|
+
"files" => files
|
|
29
|
+
},
|
|
30
|
+
"options" => sorted_hash(options),
|
|
31
|
+
"artifacts" => artifacts.map { |artifact| artifact_entry(artifact) }
|
|
32
|
+
}
|
|
33
|
+
"#{JSON.pretty_generate(document)}\n"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Validate the manifest shape and, by default, every published artifact digest.
|
|
37
|
+
# @rbs (String source, ?verify_artifacts: bool) -> Hash[String, untyped]
|
|
38
|
+
def validate(source, verify_artifacts: true)
|
|
39
|
+
document = JSON.parse(source)
|
|
40
|
+
validate_document(document)
|
|
41
|
+
verify_entries(document.fetch("artifacts")) if verify_artifacts
|
|
42
|
+
document
|
|
43
|
+
rescue JSON::ParserError, KeyError, TypeError, ArgumentError => e
|
|
44
|
+
raise Ibex::Error, "(manifest):1:1: invalid generation manifest: #{e.message}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @rbs (String path, ?verify_artifacts: bool) -> Hash[String, untyped]
|
|
48
|
+
def validate_file(path, verify_artifacts: true)
|
|
49
|
+
validate(File.binread(path), verify_artifacts: verify_artifacts)
|
|
50
|
+
rescue SystemCallError, ArgumentError => e
|
|
51
|
+
raise Ibex::Error, "#{path}:1:1: cannot read generation manifest: #{e.message}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @rbs (Artifact artifact) -> Hash[String, untyped]
|
|
55
|
+
def artifact_entry(artifact)
|
|
56
|
+
{
|
|
57
|
+
"kind" => artifact.kind.to_s,
|
|
58
|
+
"path" => File.expand_path(artifact.path),
|
|
59
|
+
"sha256" => Digest::SHA256.hexdigest(artifact.content),
|
|
60
|
+
"bytesize" => artifact.content.bytesize
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
private_class_method :artifact_entry
|
|
64
|
+
|
|
65
|
+
# @rbs (Array[Hash[String, untyped]] files) -> String
|
|
66
|
+
def input_digest(files)
|
|
67
|
+
digest = Digest::SHA256.new
|
|
68
|
+
files.each do |entry|
|
|
69
|
+
digest << entry.fetch("path") << "\0"
|
|
70
|
+
digest << entry.fetch("sha256") << "\0"
|
|
71
|
+
digest << entry.fetch("bytesize").to_s << "\0"
|
|
72
|
+
end
|
|
73
|
+
digest.hexdigest
|
|
74
|
+
end
|
|
75
|
+
private_class_method :input_digest
|
|
76
|
+
|
|
77
|
+
# @rbs (Hash[String, untyped] value) -> Hash[String, untyped]
|
|
78
|
+
def sorted_hash(value)
|
|
79
|
+
value.keys.sort.to_h do |key|
|
|
80
|
+
child = value.fetch(key)
|
|
81
|
+
normalized = child.is_a?(Hash) ? sorted_hash(child) : child
|
|
82
|
+
[key, normalized]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
private_class_method :sorted_hash
|
|
86
|
+
|
|
87
|
+
# @rbs (untyped document) -> void
|
|
88
|
+
def validate_document(document)
|
|
89
|
+
raise TypeError, "document must be an object" unless document.is_a?(Hash)
|
|
90
|
+
|
|
91
|
+
exact_keys!(document, ROOT_KEYS, "document")
|
|
92
|
+
validate_identity(document)
|
|
93
|
+
validate_input(document.fetch("input"))
|
|
94
|
+
artifacts = document.fetch("artifacts")
|
|
95
|
+
raise TypeError, "options must be an object" unless document.fetch("options").is_a?(Hash)
|
|
96
|
+
raise TypeError, "artifacts must be an array" unless artifacts.is_a?(Array)
|
|
97
|
+
raise TypeError, "artifacts must not be empty" if artifacts.empty?
|
|
98
|
+
|
|
99
|
+
validate_entries(artifacts, "artifact", require_kind: true)
|
|
100
|
+
end
|
|
101
|
+
private_class_method :validate_document
|
|
102
|
+
|
|
103
|
+
# @rbs (Hash[untyped, untyped] document) -> void
|
|
104
|
+
def validate_identity(document)
|
|
105
|
+
raise TypeError, "unsupported manifest identifier" unless document["ibex_manifest"] == IDENTIFIER
|
|
106
|
+
raise TypeError, "unsupported schema_version" unless document["schema_version"] == SCHEMA_VERSION
|
|
107
|
+
end
|
|
108
|
+
private_class_method :validate_identity
|
|
109
|
+
|
|
110
|
+
# @rbs (untyped input) -> void
|
|
111
|
+
def validate_input(input)
|
|
112
|
+
raise TypeError, "input must be an object" unless input.is_a?(Hash)
|
|
113
|
+
|
|
114
|
+
exact_keys!(input, INPUT_KEYS, "input")
|
|
115
|
+
|
|
116
|
+
files = input.fetch("files")
|
|
117
|
+
raise TypeError, "input files must be a non-empty array" unless files.is_a?(Array) && !files.empty?
|
|
118
|
+
|
|
119
|
+
validate_entries(files, "input file")
|
|
120
|
+
expected = input_digest(files)
|
|
121
|
+
root = input.fetch("root")
|
|
122
|
+
raise TypeError, "input root must be a non-empty string" unless root.is_a?(String) && !root.empty?
|
|
123
|
+
raise TypeError, "input digest mismatch" unless input.fetch("sha256") == expected
|
|
124
|
+
raise TypeError, "input root mismatch" unless root == files.fetch(0).fetch("path")
|
|
125
|
+
end
|
|
126
|
+
private_class_method :validate_input
|
|
127
|
+
|
|
128
|
+
# @rbs (Array[untyped] entries, String label, ?require_kind: bool) -> void
|
|
129
|
+
def validate_entries(entries, label, require_kind: false)
|
|
130
|
+
entries.each { |entry| validate_entry(entry, label, require_kind) }
|
|
131
|
+
end
|
|
132
|
+
private_class_method :validate_entries
|
|
133
|
+
|
|
134
|
+
# @rbs (untyped entry, String label, bool require_kind) -> void
|
|
135
|
+
def validate_entry(entry, label, require_kind)
|
|
136
|
+
raise TypeError, "#{label} must be an object" unless entry.is_a?(Hash)
|
|
137
|
+
|
|
138
|
+
exact_keys!(entry, require_kind ? ARTIFACT_KEYS : FILE_KEYS, label)
|
|
139
|
+
validate_path(entry.fetch("path"), label)
|
|
140
|
+
validate_kind(entry.fetch("kind"), label) if require_kind
|
|
141
|
+
validate_digest(entry.fetch("sha256"), label)
|
|
142
|
+
bytesize = entry.fetch("bytesize")
|
|
143
|
+
return if bytesize.is_a?(Integer) && bytesize >= 0
|
|
144
|
+
|
|
145
|
+
raise TypeError, "#{label} bytesize must be non-negative"
|
|
146
|
+
end
|
|
147
|
+
private_class_method :validate_entry
|
|
148
|
+
|
|
149
|
+
# @rbs (untyped path, String label) -> void
|
|
150
|
+
def validate_path(path, label)
|
|
151
|
+
raise TypeError, "#{label} path must be a non-empty string" unless path.is_a?(String) && !path.empty?
|
|
152
|
+
return unless path.include?("\0") || path.match?(/[[:cntrl:]]/)
|
|
153
|
+
|
|
154
|
+
raise TypeError, "#{label} path must not contain control characters"
|
|
155
|
+
end
|
|
156
|
+
private_class_method :validate_path
|
|
157
|
+
|
|
158
|
+
# @rbs (untyped kind, String label) -> void
|
|
159
|
+
def validate_kind(kind, label)
|
|
160
|
+
return if kind.is_a?(String) && !kind.empty?
|
|
161
|
+
|
|
162
|
+
raise TypeError, "#{label} kind must be a non-empty string"
|
|
163
|
+
end
|
|
164
|
+
private_class_method :validate_kind
|
|
165
|
+
|
|
166
|
+
# @rbs (untyped digest, String label) -> void
|
|
167
|
+
def validate_digest(digest, label)
|
|
168
|
+
return if digest.is_a?(String) && digest.match?(/\A[0-9a-f]{64}\z/)
|
|
169
|
+
|
|
170
|
+
raise TypeError, "#{label} digest must be SHA-256"
|
|
171
|
+
end
|
|
172
|
+
private_class_method :validate_digest
|
|
173
|
+
|
|
174
|
+
# @rbs (Hash[untyped, untyped] value, Array[String] expected, String label) -> void
|
|
175
|
+
def exact_keys!(value, expected, label)
|
|
176
|
+
keys = value.keys
|
|
177
|
+
raise TypeError, "#{label} keys must be strings" unless keys.all?(String)
|
|
178
|
+
|
|
179
|
+
extras = keys - expected
|
|
180
|
+
missing = expected - keys
|
|
181
|
+
raise TypeError, "#{label} has unknown property #{extras.fetch(0)}" unless extras.empty?
|
|
182
|
+
raise KeyError, "#{label} is missing #{missing.fetch(0)}" unless missing.empty?
|
|
183
|
+
end
|
|
184
|
+
private_class_method :exact_keys!
|
|
185
|
+
|
|
186
|
+
# @rbs (Array[untyped] entries) -> void
|
|
187
|
+
def verify_entries(entries)
|
|
188
|
+
entries.each do |entry|
|
|
189
|
+
path = entry.fetch("path")
|
|
190
|
+
source = File.binread(path)
|
|
191
|
+
raise TypeError, "artifact bytesize mismatch for #{path}" unless source.bytesize == entry.fetch("bytesize")
|
|
192
|
+
raise TypeError, "artifact digest mismatch for #{path}" unless
|
|
193
|
+
Digest::SHA256.hexdigest(source) == entry.fetch("sha256")
|
|
194
|
+
end
|
|
195
|
+
rescue SystemCallError => e
|
|
196
|
+
raise TypeError, "cannot read artifact: #{e.message}"
|
|
197
|
+
end
|
|
198
|
+
private_class_method :verify_entries
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "digest"
|
|
5
|
+
require_relative "error"
|
|
6
|
+
require_relative "generation_transaction_recovery"
|
|
7
|
+
require_relative "generation_transaction_validation"
|
|
8
|
+
|
|
9
|
+
module Ibex
|
|
10
|
+
# Publishes a rendered ArtifactSet with rollback and a manifest-last marker.
|
|
11
|
+
class GenerationTransaction
|
|
12
|
+
include GenerationTransactionRecovery
|
|
13
|
+
include GenerationTransactionValidation
|
|
14
|
+
|
|
15
|
+
class SourceChanged < Ibex::Error; end
|
|
16
|
+
|
|
17
|
+
class Error < Ibex::Error
|
|
18
|
+
attr_reader :rollback_failed #: bool
|
|
19
|
+
attr_reader :recovery_artifacts #: Array[String]
|
|
20
|
+
|
|
21
|
+
# @rbs (String message, ?rollback_failed: bool, ?recovery_artifacts: Array[String]) -> void
|
|
22
|
+
def initialize(message, rollback_failed: false, recovery_artifacts: [])
|
|
23
|
+
super(message)
|
|
24
|
+
@rollback_failed = rollback_failed
|
|
25
|
+
@recovery_artifacts = recovery_artifacts.freeze
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# rubocop:disable Layout/LineLength
|
|
30
|
+
# @rbs (ArtifactSet artifacts, ?warning: ^(String) -> void, ?stability_check: ^() -> bool, ?source_records: Array[GenerationInput], ?lock_sleeper: ^(Float) -> void) -> void
|
|
31
|
+
def initialize(artifacts, warning: ->(_message) {}, stability_check: -> { true }, source_records: [],
|
|
32
|
+
lock_sleeper: ->(seconds) { sleep(seconds) })
|
|
33
|
+
@artifacts = artifacts
|
|
34
|
+
@warning = warning
|
|
35
|
+
@stability_check = stability_check
|
|
36
|
+
@source_records = source_records
|
|
37
|
+
@lock_sleeper = lock_sleeper
|
|
38
|
+
@locks = [] #: Array[File]
|
|
39
|
+
@locked_paths = [] #: Array[String]
|
|
40
|
+
@records = [] #: Array[Hash[Symbol, untyped]]
|
|
41
|
+
@committed = false
|
|
42
|
+
@temporary_sequence = 0
|
|
43
|
+
end
|
|
44
|
+
# rubocop:enable Layout/LineLength
|
|
45
|
+
|
|
46
|
+
# @rbs () -> void
|
|
47
|
+
def commit
|
|
48
|
+
@committed = false
|
|
49
|
+
prepare_records
|
|
50
|
+
validate_source_boundaries!
|
|
51
|
+
acquire_locks
|
|
52
|
+
prepare_records
|
|
53
|
+
validate_source_boundaries!
|
|
54
|
+
validate_locked_targets!
|
|
55
|
+
ensure_sources_stable!("before staging outputs")
|
|
56
|
+
stage_records
|
|
57
|
+
ensure_sources_stable!("while staging outputs")
|
|
58
|
+
backup_records
|
|
59
|
+
sync_directories!
|
|
60
|
+
install_group(:non_marker)
|
|
61
|
+
sync_directories!
|
|
62
|
+
ensure_sources_stable!("before parser publication")
|
|
63
|
+
install_group(:parser)
|
|
64
|
+
sync_directories!
|
|
65
|
+
ensure_sources_stable!("after parser publication")
|
|
66
|
+
install_group(:manifest)
|
|
67
|
+
sync_directories!
|
|
68
|
+
ensure_sources_stable!("after manifest publication")
|
|
69
|
+
@committed = true
|
|
70
|
+
cleanup_success
|
|
71
|
+
rescue StandardError, SignalException => e
|
|
72
|
+
@committed ? report_post_commit_failure(e) : handle_failure(e)
|
|
73
|
+
ensure
|
|
74
|
+
release_locks
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# @rbs () -> void
|
|
80
|
+
def prepare_records
|
|
81
|
+
@records = @artifacts.map do |artifact|
|
|
82
|
+
target = resolved_target(artifact.path)
|
|
83
|
+
{
|
|
84
|
+
artifact: artifact, target: target, directory: File.dirname(target),
|
|
85
|
+
stage: nil, backup: nil, backed_up: false, installed: false,
|
|
86
|
+
existed: File.exist?(target), mode: target_mode(artifact, target)
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
grouped = @records.group_by do |record|
|
|
90
|
+
portable_target_key(record.fetch(:target))
|
|
91
|
+
end
|
|
92
|
+
collision = grouped.find { |_key, records| records.length > 1 }
|
|
93
|
+
return unless collision
|
|
94
|
+
|
|
95
|
+
kinds = collision.fetch(1).map { |record| record.fetch(:artifact).kind }.join(", ")
|
|
96
|
+
targets = collision.fetch(1).map { |record| record.fetch(:target) }.join(", ")
|
|
97
|
+
raise Error, "(generation):1:1: artifact targets collide: #{targets} (#{kinds})"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @rbs (Artifact artifact, String target) -> Integer
|
|
101
|
+
def target_mode(artifact, target)
|
|
102
|
+
return artifact.mode if artifact.mode
|
|
103
|
+
return File.stat(target).mode & 0o7777 if File.exist?(target)
|
|
104
|
+
|
|
105
|
+
mask = File.umask
|
|
106
|
+
File.umask(mask)
|
|
107
|
+
0o666 & ~mask
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @rbs () -> void
|
|
111
|
+
def acquire_locks
|
|
112
|
+
lock_paths = @records.map { |record| lock_path(record.fetch(:target)) }.uniq.sort
|
|
113
|
+
validate_lock_boundaries!(lock_paths)
|
|
114
|
+
@locked_paths = lock_paths
|
|
115
|
+
@locks = [] #: Array[File]
|
|
116
|
+
lock_paths.each do |path|
|
|
117
|
+
flags = File::RDWR | File::CREAT
|
|
118
|
+
flags |= File.const_get(:NOFOLLOW) if File.const_defined?(:NOFOLLOW)
|
|
119
|
+
lock = File.new(path, flags, 0o600)
|
|
120
|
+
@locks << lock
|
|
121
|
+
validate_lock_identity!(path, lock)
|
|
122
|
+
acquire_lock(lock, path)
|
|
123
|
+
validate_lock_identity!(path, lock)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @rbs (File lock, String path) -> void
|
|
128
|
+
def acquire_lock(lock, path)
|
|
129
|
+
until lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
130
|
+
unless @stability_check.call
|
|
131
|
+
raise SourceChanged, "(generation):1:1: generation cancelled while waiting for lock: #{path}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@lock_sleeper.call(0.05)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @rbs (String target) -> String
|
|
139
|
+
def lock_path(target)
|
|
140
|
+
digest = Digest::SHA256.hexdigest(target).slice(0, 16)
|
|
141
|
+
File.join(File.dirname(target), ".ibex-generation-#{digest}.lock")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @rbs () -> void
|
|
145
|
+
def stage_records
|
|
146
|
+
@records.each do |record|
|
|
147
|
+
temporary = create_temporary_file(record.fetch(:directory), ".stage")
|
|
148
|
+
begin
|
|
149
|
+
record[:stage] = temporary.path.dup
|
|
150
|
+
temporary.binmode
|
|
151
|
+
temporary.write(record.fetch(:artifact).content)
|
|
152
|
+
temporary.flush
|
|
153
|
+
temporary.chmod(record.fetch(:mode))
|
|
154
|
+
temporary.fsync
|
|
155
|
+
ensure
|
|
156
|
+
temporary.close
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @rbs () -> void
|
|
162
|
+
def backup_records
|
|
163
|
+
@records.each do |record|
|
|
164
|
+
target = record.fetch(:target)
|
|
165
|
+
next unless File.exist?(target)
|
|
166
|
+
|
|
167
|
+
validate_existing_target!(target)
|
|
168
|
+
backup = create_backup_link(target, record.fetch(:directory))
|
|
169
|
+
record[:backup] = backup
|
|
170
|
+
record[:backed_up] = true
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @rbs (String directory, String suffix) -> File
|
|
175
|
+
def create_temporary_file(directory, suffix)
|
|
176
|
+
with_temporary_path(directory, suffix) do |path|
|
|
177
|
+
flags = File::WRONLY | File::CREAT | File::EXCL
|
|
178
|
+
flags |= File.const_get(:NOFOLLOW) if File.const_defined?(:NOFOLLOW)
|
|
179
|
+
return File.new(path, flags, 0o600)
|
|
180
|
+
end
|
|
181
|
+
raise Error, "(generation):1:1: temporary file is unavailable in #{directory}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# @rbs (String target, String directory) -> String
|
|
185
|
+
def create_backup_link(target, directory)
|
|
186
|
+
with_temporary_path(directory, ".backup") do |path|
|
|
187
|
+
File.link(target, path)
|
|
188
|
+
return path
|
|
189
|
+
end
|
|
190
|
+
raise Error, "(generation):1:1: backup path is unavailable in #{directory}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @rbs (String directory, String suffix) { (String) -> void } -> void
|
|
194
|
+
def with_temporary_path(directory, suffix)
|
|
195
|
+
100.times do
|
|
196
|
+
@temporary_sequence += 1
|
|
197
|
+
name = ".ibex-generation-#{Process.pid}-#{object_id.to_s(36)}-#{@temporary_sequence.to_s(36)}#{suffix}"
|
|
198
|
+
yield File.join(directory, name)
|
|
199
|
+
rescue Errno::EEXIST
|
|
200
|
+
next
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# @rbs (Symbol group) -> void
|
|
205
|
+
def install_group(group)
|
|
206
|
+
selected = @records.select { |record| publication_group(record.fetch(:artifact)) == group }
|
|
207
|
+
selected.sort_by { |record| record.fetch(:target) }.each do |record|
|
|
208
|
+
File.rename(record.fetch(:stage), record.fetch(:target))
|
|
209
|
+
record[:stage] = nil
|
|
210
|
+
record[:installed] = true
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# @rbs (Artifact artifact) -> Symbol
|
|
215
|
+
def publication_group(artifact)
|
|
216
|
+
return :manifest if artifact.kind == :manifest
|
|
217
|
+
return :parser if artifact.kind == :parser
|
|
218
|
+
|
|
219
|
+
:non_marker
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# @rbs () -> void
|
|
223
|
+
def sync_directories!
|
|
224
|
+
@records.map { |record| record.fetch(:directory) }.uniq.sort.each do |directory|
|
|
225
|
+
File.open(directory, File::RDONLY, &:fsync)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# @rbs () -> void
|
|
230
|
+
def cleanup_success
|
|
231
|
+
failures = cleanup_artifacts(preserve_backups: false)
|
|
232
|
+
sync_failures = sync_directories_best_effort
|
|
233
|
+
messages = failures + sync_failures
|
|
234
|
+
warn_best_effort("generation cleanup incomplete: #{messages.join('; ')}") unless messages.empty?
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# @rbs (Exception error) -> void
|
|
238
|
+
def report_post_commit_failure(error)
|
|
239
|
+
warn_best_effort("generation committed but cleanup failed: #{error.message}")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# @rbs (String message) -> void
|
|
243
|
+
def warn_best_effort(message)
|
|
244
|
+
@warning.call(message)
|
|
245
|
+
rescue StandardError
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# @rbs () -> void
|
|
250
|
+
def release_locks
|
|
251
|
+
@locks.reverse_each do |lock|
|
|
252
|
+
lock.flock(File::LOCK_UN)
|
|
253
|
+
lock.close
|
|
254
|
+
rescue StandardError
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
@locks = [] #: Array[File]
|
|
258
|
+
@locked_paths = [] #: Array[String]
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ibex
|
|
5
|
+
# Failure recovery and best-effort cleanup for GenerationTransaction.
|
|
6
|
+
module GenerationTransactionRecovery
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# @rbs (Exception cause) -> bot
|
|
10
|
+
def handle_failure(cause)
|
|
11
|
+
rollback_failures = rollback
|
|
12
|
+
cleanup_failures = cleanup_artifacts(preserve_backups: !rollback_failures.empty?)
|
|
13
|
+
sync_failures = sync_directories_best_effort
|
|
14
|
+
raise cause if source_change_clean?(cause, rollback_failures, cleanup_failures, sync_failures)
|
|
15
|
+
|
|
16
|
+
recovery = recovery_artifacts
|
|
17
|
+
details = failure_details(cause, rollback_failures, cleanup_failures, sync_failures, recovery)
|
|
18
|
+
raise GenerationTransaction::Error.new(
|
|
19
|
+
"(generation):1:1: #{details.join(' | ')}",
|
|
20
|
+
rollback_failed: !rollback_failures.empty?, recovery_artifacts: recovery
|
|
21
|
+
), cause: cause
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @rbs (Exception cause, Array[String] rollback, Array[String] cleanup, Array[String] sync) -> bool
|
|
25
|
+
def source_change_clean?(cause, rollback, cleanup, sync)
|
|
26
|
+
cause.is_a?(GenerationTransaction::SourceChanged) && rollback.empty? && cleanup.empty? && sync.empty?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# rubocop:disable Layout/LineLength
|
|
30
|
+
# @rbs (Exception cause, Array[String] rollback, Array[String] cleanup, Array[String] sync, Array[String] recovery) -> Array[String]
|
|
31
|
+
def failure_details(cause, rollback, cleanup, sync, recovery)
|
|
32
|
+
details = [cause.message]
|
|
33
|
+
details << "rollback failed: #{rollback.join('; ')}" unless rollback.empty?
|
|
34
|
+
details << "cleanup failed: #{cleanup.join('; ')}" unless cleanup.empty?
|
|
35
|
+
details << "directory sync failed: #{sync.join('; ')}" unless sync.empty?
|
|
36
|
+
details << "recovery artifacts: #{recovery.join(', ')}" unless recovery.empty?
|
|
37
|
+
details
|
|
38
|
+
end
|
|
39
|
+
# rubocop:enable Layout/LineLength
|
|
40
|
+
|
|
41
|
+
# @rbs () -> Array[String]
|
|
42
|
+
def recovery_artifacts
|
|
43
|
+
@records.filter_map do |record|
|
|
44
|
+
backup = record[:backup]
|
|
45
|
+
backup if backup && File.exist?(backup)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @rbs () -> Array[String]
|
|
50
|
+
def rollback
|
|
51
|
+
failures = [] #: Array[String]
|
|
52
|
+
@records.reverse_each do |record|
|
|
53
|
+
rollback_record(record)
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
failures << "#{record.fetch(:target)}: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
failures
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @rbs (Hash[Symbol, untyped] record) -> void
|
|
61
|
+
def rollback_record(record)
|
|
62
|
+
target = record.fetch(:target)
|
|
63
|
+
if record.fetch(:backed_up)
|
|
64
|
+
if record.fetch(:installed)
|
|
65
|
+
File.rename(record.fetch(:backup), target)
|
|
66
|
+
else
|
|
67
|
+
File.unlink(record.fetch(:backup))
|
|
68
|
+
end
|
|
69
|
+
record[:backup] = nil
|
|
70
|
+
record[:backed_up] = false
|
|
71
|
+
record[:installed] = false
|
|
72
|
+
elsif record.fetch(:installed)
|
|
73
|
+
File.unlink(target)
|
|
74
|
+
record[:installed] = false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @rbs (preserve_backups: bool) -> Array[String]
|
|
79
|
+
def cleanup_artifacts(preserve_backups:)
|
|
80
|
+
failures = [] #: Array[String]
|
|
81
|
+
@records.each do |record|
|
|
82
|
+
paths = [record[:stage]]
|
|
83
|
+
paths << record[:backup] unless preserve_backups
|
|
84
|
+
paths.compact.each { |path| remove_artifact(path, failures) }
|
|
85
|
+
end
|
|
86
|
+
failures
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @rbs (String path, Array[String] failures) -> void
|
|
90
|
+
def remove_artifact(path, failures)
|
|
91
|
+
File.unlink(path)
|
|
92
|
+
rescue Errno::ENOENT
|
|
93
|
+
nil
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
failures << "#{path}: #{e.message}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @rbs () -> Array[String]
|
|
99
|
+
def sync_directories_best_effort
|
|
100
|
+
failures = [] #: Array[String]
|
|
101
|
+
@records.map { |record| record.fetch(:directory) }.uniq.sort.each do |directory|
|
|
102
|
+
File.open(directory, File::RDONLY, &:fsync)
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
failures << "#{directory}: #{e.message}"
|
|
105
|
+
end
|
|
106
|
+
failures
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|