yrby 0.7.0 → 0.8.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/CHANGELOG.md +71 -0
- data/Cargo.lock +29 -29
- data/README.md +582 -380
- data/ext/yrby/Cargo.toml +3 -3
- data/ext/yrby/crates/html-core/Cargo.toml +5 -4
- data/ext/yrby/crates/html-core/src/lib.rs +16 -20
- data/ext/yrby/crates/lexical-html/Cargo.toml +5 -4
- data/ext/yrby/crates/lexical-html/src/lib.rs +57 -48
- data/ext/yrby/crates/prosemirror-html/Cargo.toml +5 -4
- data/ext/yrby/crates/prosemirror-html/src/lib.rs +151 -104
- data/ext/yrby/src/lib.rs +30 -15
- data/ext/yrby/src/protocol.rs +17 -17
- data/ext/yrby/src/read.rs +137 -26
- data/lib/generators/yrby/install/install_generator.rb +24 -13
- data/lib/generators/yrby/install/templates/document_channel.rb +4 -7
- data/lib/generators/yrby/tables/tables_generator.rb +7 -4
- data/lib/generators/yrby/tables/templates/create_y_tables.rb +1 -1
- data/lib/y/collaborative/attribute.rb +53 -0
- data/lib/y/collaborative/helper.rb +48 -0
- data/lib/y/collaborative.rb +98 -0
- data/lib/y/lexxy.rb +2 -2
- data/lib/y/rendering.rb +15 -15
- data/lib/y/tiptap.rb +3 -3
- data/lib/y/version.rb +1 -1
- data/lib/y.rb +1 -1
- metadata +4 -9
- data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/common.rs +0 -355
- data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/dynamic.rs +0 -276
- data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/macros.rs +0 -49
- data/ext/yrby/target/debug/build/rb-sys-4407948463231c4f/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +0 -8934
- data/ext/yrby/target/debug/build/rb-sys-dbeea42737529c2d/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +0 -8934
- data/ext/yrby/target/debug/build/serde-58ea0ee887cc2602/out/private.rs +0 -6
- data/ext/yrby/target/debug/build/serde_core-41f407c21c1f205e/out/private.rs +0 -5
- data/ext/yrby/target/debug/build/thiserror-0f1416a82ff26f22/out/private.rs +0 -5
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
require "global_id"
|
|
5
|
+
|
|
6
|
+
module Y
|
|
7
|
+
# The signed token that connects a page to a channel for record-backed
|
|
8
|
+
# collaborative documents.
|
|
9
|
+
#
|
|
10
|
+
# The client never picks its own document. The server signs a token for the
|
|
11
|
+
# record and attribute, the page renders it, and the channel looks the
|
|
12
|
+
# record up from it. The token is a signed GlobalID scoped to one attribute.
|
|
13
|
+
#
|
|
14
|
+
# # the view
|
|
15
|
+
# tag.div data: { grant: post.collaborative_sgid(:body) }
|
|
16
|
+
#
|
|
17
|
+
# # the channel
|
|
18
|
+
# def authorized?(_key)
|
|
19
|
+
# record.present? && record.editable_by?(current_user)
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# # Not memoized, because AnyCable gives each command a fresh channel
|
|
23
|
+
# # instance and a cached record would go stale. Y::DocumentChannel
|
|
24
|
+
# # does the same.
|
|
25
|
+
# def record
|
|
26
|
+
# Y::Collaborative.locate(params[:grant], :body)
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# The engine includes this into ActiveRecord::Base so any channel's
|
|
30
|
+
# authorized? can use it. lexxy-realtime uses the same token flow.
|
|
31
|
+
module Collaborative
|
|
32
|
+
extend ActiveSupport::Concern
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# The signed-GlobalID purpose for one collaborative attribute. A token
|
|
36
|
+
# signed for one attribute verifies only against that attribute's
|
|
37
|
+
# purpose, so it can't locate the record through any other attribute.
|
|
38
|
+
# The purpose leaves out the channel name, so a custom channel and the
|
|
39
|
+
# shipped one can both resolve the same tokens for an attribute.
|
|
40
|
+
def sgid_purpose(name) = "yrby/#{name}"
|
|
41
|
+
|
|
42
|
+
# Looks up the record for a token from `collaborative_sgid(name)`.
|
|
43
|
+
# Returns nil for an invalid, tampered, expired, or wrong-attribute
|
|
44
|
+
# token, and for a record that no longer exists.
|
|
45
|
+
def locate(sgid, name)
|
|
46
|
+
GlobalID::Locator.locate_signed(sgid, for: sgid_purpose(name))
|
|
47
|
+
rescue ActiveRecord::RecordNotFound
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
included do
|
|
53
|
+
class_attribute :collaborative_document_options,
|
|
54
|
+
instance_accessor: false, default: {}.freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class_methods do
|
|
58
|
+
# Declares how one attribute's document is stored. The shipped channel
|
|
59
|
+
# and Ruby reads both follow it, and undeclared attributes use plain
|
|
60
|
+
# Y::Document.
|
|
61
|
+
def has_collaborative_document(name, encrypted: false) # rubocop:disable Naming/PredicatePrefix
|
|
62
|
+
self.collaborative_document_options = collaborative_document_options.merge(
|
|
63
|
+
name.to_s => { encrypted: encrypted }.freeze
|
|
64
|
+
).freeze
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns the model that stores this attribute's document,
|
|
68
|
+
# Y::EncryptedDocument for an attribute declared encrypted and
|
|
69
|
+
# Y::Document for the rest. It resolves the constant on every call so
|
|
70
|
+
# that declaring an attribute doesn't load the engine's models while the
|
|
71
|
+
# app's models are still loading.
|
|
72
|
+
def collaborative_document_class(name)
|
|
73
|
+
encrypted = collaborative_document_options.dig(name.to_s, :encrypted)
|
|
74
|
+
encrypted ? Y::EncryptedDocument : Y::Document
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns the document for one attribute, stored the way the model
|
|
79
|
+
# declared. It responds to load_state, append, y_doc, and key.
|
|
80
|
+
def collaborative_document(name)
|
|
81
|
+
Attribute.new(self, name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A signed token that a channel can pass to Y::Collaborative.locate to get
|
|
85
|
+
# this record back, for this attribute only. Pass expires_in: to limit how
|
|
86
|
+
# long the grant lasts. Without it GlobalID's own default applies, which is
|
|
87
|
+
# one month under Rails. We pass the option through only when it's given,
|
|
88
|
+
# because an explicit nil would mean "never expire".
|
|
89
|
+
def collaborative_sgid(name, expires_in: nil)
|
|
90
|
+
options = { for: Y::Collaborative.sgid_purpose(name) }
|
|
91
|
+
options[:expires_in] = expires_in if expires_in
|
|
92
|
+
to_sgid(**options).to_s
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
require "y/collaborative/attribute"
|
|
98
|
+
require "y/collaborative/helper"
|
data/lib/y/lexxy.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Y
|
|
4
4
|
# The Lexxy renderer: Y::Lexical (core Lexical) plus the Lexxy-specific
|
|
5
|
-
# schema, applied beneath the app's rules
|
|
5
|
+
# schema, applied beneath the app's rules: an app rule for one of these
|
|
6
6
|
# types simply replaces it. This is the byte-parity class: the fixture
|
|
7
7
|
# tests hold `Y::Lexxy.new(doc).to_html` identical to a live editor's own
|
|
8
8
|
# serialized value.
|
|
@@ -36,7 +36,7 @@ module Y
|
|
|
36
36
|
%(<th class="lexxy-content__table-cell--header" #{style}>#{node.content}</th>)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
# Attribute order follows Lexxy's export
|
|
39
|
+
# Attribute order follows Lexxy's export: checked items put aria-checked
|
|
40
40
|
# before value; items holding a nested list append the
|
|
41
41
|
# lexxy-nested-listitem class after value.
|
|
42
42
|
def self.list_item(node)
|
data/lib/y/rendering.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Y
|
|
|
10
10
|
# `marks:`. A rule is consulted before the built-in schema, so it can add a
|
|
11
11
|
# custom node or override a built-in one.
|
|
12
12
|
#
|
|
13
|
-
# The usual way in is the block form
|
|
13
|
+
# The usual way in is the block form, one `rules.node` call per type,
|
|
14
14
|
# keyword options for markup-as-data, a Ruby block for logic (see Builder
|
|
15
15
|
# below). `contains:` says what's inside the node: :inline (formatted
|
|
16
16
|
# text, the default), :blocks (child block nodes), or :none (a leaf). The
|
|
@@ -26,7 +26,7 @@ module Y
|
|
|
26
26
|
# attrs: { "class" => ["callout callout--", :kind] },
|
|
27
27
|
# contains: :blocks
|
|
28
28
|
# end
|
|
29
|
-
# `tag` is the element; `attrs` values are templates
|
|
29
|
+
# `tag` is the element; `attrs` values are templates: a String literal,
|
|
30
30
|
# a Symbol referencing one of the node's stored attributes, or an Array
|
|
31
31
|
# mixing both (an attribute that resolves empty is omitted); `text` is a
|
|
32
32
|
# template for literal text content; `void: true` emits no closing tag;
|
|
@@ -45,20 +45,20 @@ module Y
|
|
|
45
45
|
# document is locked) and receives a RenderRules::Node with the node's
|
|
46
46
|
# type, stored attributes, children already rendered to HTML, and
|
|
47
47
|
# child_types (its element/block children by type). Its return value is
|
|
48
|
-
# spliced in verbatim
|
|
48
|
+
# spliced in verbatim: it is trusted HTML, so escape any attribute
|
|
49
49
|
# values you interpolate.
|
|
50
50
|
#
|
|
51
51
|
# Mark rules (ProseMirror only) are declarative: `tag` plus `attrs`
|
|
52
52
|
# templates whose Symbol refs resolve against the mark's own attributes. A
|
|
53
53
|
# custom mark wraps outside every built-in mark; several custom marks nest
|
|
54
54
|
# alphabetically. A rule for a built-in mark's stored name replaces its
|
|
55
|
-
# wrap (the markup changes, the semantics don't
|
|
55
|
+
# wrap (the markup changes, the semantics don't: an overridden code mark
|
|
56
56
|
# still excludes the other formatting).
|
|
57
57
|
module RenderRules
|
|
58
58
|
# What a callback receives. `attrs` keys are as stored (Lexical's own
|
|
59
59
|
# props keep their "__" prefix); `content` is the node's children,
|
|
60
60
|
# already rendered to an HTML string; `child_types` lists the node's
|
|
61
|
-
# element/block children by type, in document order
|
|
61
|
+
# element/block children by type, in document order: the structural
|
|
62
62
|
# facts attrs and content can't answer (a gallery's image count, whether
|
|
63
63
|
# a list item holds a nested list).
|
|
64
64
|
Node = Data.define(:type, :attrs, :content, :child_types)
|
|
@@ -137,7 +137,7 @@ module Y
|
|
|
137
137
|
# from stored values. Text content escapes `&`, `<`, `>` (quotes stay
|
|
138
138
|
# literal, matching the browser serializer); attribute values also
|
|
139
139
|
# escape `"`. Prefer these over ERB::Util.html_escape when byte parity
|
|
140
|
-
# with editor output matters
|
|
140
|
+
# with editor output matters: html_escape also rewrites apostrophes.
|
|
141
141
|
def escape_text(value)
|
|
142
142
|
value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
143
143
|
end
|
|
@@ -212,10 +212,10 @@ module Y
|
|
|
212
212
|
# they compile the rules config, hold the callbacks, and splice deferred
|
|
213
213
|
# segments after a render. The native handle does everything else.
|
|
214
214
|
class Lexical
|
|
215
|
-
# `Y::Lexical.new(doc, nodes: { "type" => rule })
|
|
215
|
+
# `Y::Lexical.new(doc, nodes: { "type" => rule })`; see Y::RenderRules
|
|
216
216
|
# for the rule forms. This is core Lexical only: paragraphs, headings,
|
|
217
217
|
# quotes, code, lists, tables, links, text formatting. Editor-specific
|
|
218
|
-
# nodes arrive as rules
|
|
218
|
+
# nodes arrive as rules. Y::Lexxy subclasses this with the Lexxy schema;
|
|
219
219
|
# a different Lexical editor brings its own rule set the same way.
|
|
220
220
|
def initialize(doc, nodes: {})
|
|
221
221
|
builder = RenderRules::Builder.new(marks_allowed: false)
|
|
@@ -232,10 +232,10 @@ module Y
|
|
|
232
232
|
RenderRules.splice(result, @render_callbacks)
|
|
233
233
|
end
|
|
234
234
|
|
|
235
|
-
# What node types this document actually contains
|
|
235
|
+
# What node types this document actually contains: the discovery aid
|
|
236
236
|
# for writing rules. Facts per type: "count", "attrs" (names as stored),
|
|
237
237
|
# "children" (child node types), "text" (whether it holds text runs),
|
|
238
|
-
# and "handled" ("builtin", "rule", or nil
|
|
238
|
+
# and "handled" ("builtin", "rule", or nil; nil marks the types you
|
|
239
239
|
# still need a rule for). Children plus text is how you pick contains:.
|
|
240
240
|
def node_types(root = nil)
|
|
241
241
|
json = root.nil? ? @native.node_types : @native.node_types(root)
|
|
@@ -244,11 +244,11 @@ module Y
|
|
|
244
244
|
end
|
|
245
245
|
|
|
246
246
|
class ProseMirror
|
|
247
|
-
# `Y::ProseMirror.new(doc, nodes: {...}, marks: {...})
|
|
247
|
+
# `Y::ProseMirror.new(doc, nodes: {...}, marks: {...})`; see
|
|
248
248
|
# Y::RenderRules for the rule forms. This is core ProseMirror only:
|
|
249
249
|
# prosemirror-schema-basic plus the prosemirror-tables family, and the
|
|
250
|
-
# full mark set (marks are native
|
|
251
|
-
# Editor-specific nodes arrive as rules
|
|
250
|
+
# full mark set (marks are native; see `rules.mark` for overrides).
|
|
251
|
+
# Editor-specific nodes arrive as rules. Y::Tiptap subclasses this with
|
|
252
252
|
# Tiptap's extension nodes; a different ProseMirror editor brings its
|
|
253
253
|
# own rule set the same way.
|
|
254
254
|
def initialize(doc, nodes: {}, marks: {})
|
|
@@ -267,10 +267,10 @@ module Y
|
|
|
267
267
|
RenderRules.splice(result, @render_callbacks)
|
|
268
268
|
end
|
|
269
269
|
|
|
270
|
-
# What node types this document actually contains
|
|
270
|
+
# What node types this document actually contains: the discovery aid
|
|
271
271
|
# for writing rules. Facts per type: "count", "attrs" (names as stored),
|
|
272
272
|
# "children" (child node types), "text" (whether it holds text runs),
|
|
273
|
-
# and "handled" ("builtin", "rule", or nil
|
|
273
|
+
# and "handled" ("builtin", "rule", or nil; nil marks the types you
|
|
274
274
|
# still need a rule for). Children plus text is how you pick contains:.
|
|
275
275
|
def node_types(root = nil)
|
|
276
276
|
json = root.nil? ? @native.node_types : @native.node_types(root)
|
data/lib/y/tiptap.rb
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Y
|
|
4
|
-
# The Tiptap renderer: Y::ProseMirror (core ProseMirror
|
|
5
|
-
# tables) plus Tiptap's extension nodes, applied beneath the app's rules
|
|
4
|
+
# The Tiptap renderer: Y::ProseMirror (core ProseMirror, schema-basic plus
|
|
5
|
+
# tables) plus Tiptap's extension nodes, applied beneath the app's rules,
|
|
6
6
|
# an app rule for one of these types simply replaces it. This is the
|
|
7
7
|
# byte-parity class: the fixture tests hold `Y::Tiptap.new(doc).to_html`
|
|
8
8
|
# identical to a live editor's own `getHTML()`.
|
|
9
9
|
#
|
|
10
10
|
# Tiptap's marks (underline, highlight, sub/superscript, textStyle) render
|
|
11
|
-
# natively in the base class
|
|
11
|
+
# natively in the base class: mark serialization is text-run machinery
|
|
12
12
|
# the rule system can't express.
|
|
13
13
|
class Tiptap < ProseMirror
|
|
14
14
|
# Tiptap's TaskItem markup: the data-checked flag (false when unset), a
|
data/lib/y/version.rb
CHANGED
data/lib/y.rb
CHANGED
|
@@ -20,5 +20,5 @@ require_relative "y/decoder"
|
|
|
20
20
|
module Y
|
|
21
21
|
# Doc, Error, and the protocol module functions are defined in the Rust
|
|
22
22
|
# extension. The ActionCable integration (Y::ActionCable::Sync) lives in the
|
|
23
|
-
#
|
|
23
|
+
# `yrby-rails` gem; require "y/action_cable".
|
|
24
24
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yrby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- JP Camara
|
|
@@ -92,19 +92,14 @@ files:
|
|
|
92
92
|
- ext/yrby/src/lib.rs
|
|
93
93
|
- ext/yrby/src/protocol.rs
|
|
94
94
|
- ext/yrby/src/read.rs
|
|
95
|
-
- ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/common.rs
|
|
96
|
-
- ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/dynamic.rs
|
|
97
|
-
- ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/macros.rs
|
|
98
|
-
- ext/yrby/target/debug/build/rb-sys-4407948463231c4f/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs
|
|
99
|
-
- ext/yrby/target/debug/build/rb-sys-dbeea42737529c2d/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs
|
|
100
|
-
- ext/yrby/target/debug/build/serde-58ea0ee887cc2602/out/private.rs
|
|
101
|
-
- ext/yrby/target/debug/build/serde_core-41f407c21c1f205e/out/private.rs
|
|
102
|
-
- ext/yrby/target/debug/build/thiserror-0f1416a82ff26f22/out/private.rs
|
|
103
95
|
- lib/generators/yrby/install/install_generator.rb
|
|
104
96
|
- lib/generators/yrby/install/templates/document_channel.rb
|
|
105
97
|
- lib/generators/yrby/tables/tables_generator.rb
|
|
106
98
|
- lib/generators/yrby/tables/templates/create_y_tables.rb
|
|
107
99
|
- lib/y.rb
|
|
100
|
+
- lib/y/collaborative.rb
|
|
101
|
+
- lib/y/collaborative/attribute.rb
|
|
102
|
+
- lib/y/collaborative/helper.rb
|
|
108
103
|
- lib/y/decoder.rb
|
|
109
104
|
- lib/y/lexxy.rb
|
|
110
105
|
- lib/y/rendering.rb
|
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
|
|
3
|
-
use std::cell::RefCell;
|
|
4
|
-
use std::collections::HashMap;
|
|
5
|
-
use std::env;
|
|
6
|
-
use std::path::{Path, PathBuf};
|
|
7
|
-
use std::process::Command;
|
|
8
|
-
|
|
9
|
-
use glob::{MatchOptions, Pattern};
|
|
10
|
-
|
|
11
|
-
//================================================
|
|
12
|
-
// Commands
|
|
13
|
-
//================================================
|
|
14
|
-
|
|
15
|
-
thread_local! {
|
|
16
|
-
/// The errors encountered by the build script while executing commands.
|
|
17
|
-
static COMMAND_ERRORS: RefCell<HashMap<String, Vec<String>>> = RefCell::default();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/// Adds an error encountered by the build script while executing a command.
|
|
21
|
-
fn add_command_error(name: &str, path: &str, arguments: &[&str], message: String) {
|
|
22
|
-
COMMAND_ERRORS.with(|e| {
|
|
23
|
-
e.borrow_mut()
|
|
24
|
-
.entry(name.into())
|
|
25
|
-
.or_default()
|
|
26
|
-
.push(format!(
|
|
27
|
-
"couldn't execute `{} {}` (path={}) ({})",
|
|
28
|
-
name,
|
|
29
|
-
arguments.join(" "),
|
|
30
|
-
path,
|
|
31
|
-
message,
|
|
32
|
-
))
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/// A struct that prints the errors encountered by the build script while
|
|
37
|
-
/// executing commands when dropped (unless explictly discarded).
|
|
38
|
-
///
|
|
39
|
-
/// This is handy because we only want to print these errors when the build
|
|
40
|
-
/// script fails to link to an instance of `libclang`. For example, if
|
|
41
|
-
/// `llvm-config` couldn't be executed but an instance of `libclang` was found
|
|
42
|
-
/// anyway we don't want to pollute the build output with irrelevant errors.
|
|
43
|
-
#[derive(Default)]
|
|
44
|
-
pub struct CommandErrorPrinter {
|
|
45
|
-
discard: bool,
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
impl CommandErrorPrinter {
|
|
49
|
-
pub fn discard(mut self) {
|
|
50
|
-
self.discard = true;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
impl Drop for CommandErrorPrinter {
|
|
55
|
-
fn drop(&mut self) {
|
|
56
|
-
if self.discard {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let errors = COMMAND_ERRORS.with(|e| e.borrow().clone());
|
|
61
|
-
|
|
62
|
-
if let Some(errors) = errors.get("llvm-config") {
|
|
63
|
-
println!(
|
|
64
|
-
"cargo:warning=could not execute `llvm-config` one or more \
|
|
65
|
-
times, if the LLVM_CONFIG_PATH environment variable is set to \
|
|
66
|
-
a full path to valid `llvm-config` executable it will be used \
|
|
67
|
-
to try to find an instance of `libclang` on your system: {}",
|
|
68
|
-
errors
|
|
69
|
-
.iter()
|
|
70
|
-
.map(|e| format!("\"{}\"", e))
|
|
71
|
-
.collect::<Vec<_>>()
|
|
72
|
-
.join("\n "),
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if let Some(errors) = errors.get("xcode-select") {
|
|
77
|
-
println!(
|
|
78
|
-
"cargo:warning=could not execute `xcode-select` one or more \
|
|
79
|
-
times, if a valid instance of this executable is on your PATH \
|
|
80
|
-
it will be used to try to find an instance of `libclang` on \
|
|
81
|
-
your system: {}",
|
|
82
|
-
errors
|
|
83
|
-
.iter()
|
|
84
|
-
.map(|e| format!("\"{}\"", e))
|
|
85
|
-
.collect::<Vec<_>>()
|
|
86
|
-
.join("\n "),
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
#[cfg(test)]
|
|
93
|
-
lazy_static::lazy_static! {
|
|
94
|
-
pub static ref RUN_COMMAND_MOCK: std::sync::Mutex<
|
|
95
|
-
Option<Box<dyn Fn(&str, &str, &[&str]) -> Option<String> + Send + Sync + 'static>>,
|
|
96
|
-
> = std::sync::Mutex::new(None);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/// Executes a command and returns the `stdout` output if the command was
|
|
100
|
-
/// successfully executed (errors are added to `COMMAND_ERRORS`).
|
|
101
|
-
fn run_command(name: &str, path: &str, arguments: &[&str]) -> Option<String> {
|
|
102
|
-
#[cfg(test)]
|
|
103
|
-
if let Some(command) = &*RUN_COMMAND_MOCK.lock().unwrap() {
|
|
104
|
-
return command(name, path, arguments);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
let output = match Command::new(path).args(arguments).output() {
|
|
108
|
-
Ok(output) => output,
|
|
109
|
-
Err(error) => {
|
|
110
|
-
let message = format!("error: {}", error);
|
|
111
|
-
add_command_error(name, path, arguments, message);
|
|
112
|
-
return None;
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
if output.status.success() {
|
|
117
|
-
Some(String::from_utf8_lossy(&output.stdout).into_owned())
|
|
118
|
-
} else {
|
|
119
|
-
let message = format!("exit code: {}", output.status);
|
|
120
|
-
add_command_error(name, path, arguments, message);
|
|
121
|
-
None
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/// Executes the `llvm-config` command and returns the `stdout` output if the
|
|
126
|
-
/// command was successfully executed (errors are added to `COMMAND_ERRORS`).
|
|
127
|
-
pub fn run_llvm_config(arguments: &[&str]) -> Option<String> {
|
|
128
|
-
let path = env::var("LLVM_CONFIG_PATH").unwrap_or_else(|_| "llvm-config".into());
|
|
129
|
-
run_command("llvm-config", &path, arguments)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/// Executes the `xcode-select` command and returns the `stdout` output if the
|
|
133
|
-
/// command was successfully executed (errors are added to `COMMAND_ERRORS`).
|
|
134
|
-
pub fn run_xcode_select(arguments: &[&str]) -> Option<String> {
|
|
135
|
-
run_command("xcode-select", "xcode-select", arguments)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
//================================================
|
|
139
|
-
// Search Directories
|
|
140
|
-
//================================================
|
|
141
|
-
// These search directories are listed in order of
|
|
142
|
-
// preference, so if multiple `libclang` instances
|
|
143
|
-
// are found when searching matching directories,
|
|
144
|
-
// the `libclang` instances from earlier
|
|
145
|
-
// directories will be preferred (though version
|
|
146
|
-
// takes precedence over location).
|
|
147
|
-
//================================================
|
|
148
|
-
|
|
149
|
-
/// `libclang` directory patterns for Haiku.
|
|
150
|
-
const DIRECTORIES_HAIKU: &[&str] = &[
|
|
151
|
-
"/boot/home/config/non-packaged/develop/lib",
|
|
152
|
-
"/boot/home/config/non-packaged/lib",
|
|
153
|
-
"/boot/system/non-packaged/develop/lib",
|
|
154
|
-
"/boot/system/non-packaged/lib",
|
|
155
|
-
"/boot/system/develop/lib",
|
|
156
|
-
"/boot/system/lib",
|
|
157
|
-
];
|
|
158
|
-
|
|
159
|
-
/// `libclang` directory patterns for Linux (and FreeBSD).
|
|
160
|
-
const DIRECTORIES_LINUX: &[&str] = &[
|
|
161
|
-
"/usr/local/llvm*/lib*",
|
|
162
|
-
"/usr/local/lib*/*/*",
|
|
163
|
-
"/usr/local/lib*/*",
|
|
164
|
-
"/usr/local/lib*",
|
|
165
|
-
"/usr/lib*/*/*",
|
|
166
|
-
"/usr/lib*/*",
|
|
167
|
-
"/usr/lib*",
|
|
168
|
-
];
|
|
169
|
-
|
|
170
|
-
/// `libclang` directory patterns for macOS.
|
|
171
|
-
const DIRECTORIES_MACOS: &[&str] = &[
|
|
172
|
-
"/usr/local/opt/llvm*/lib/llvm*/lib",
|
|
173
|
-
"/Library/Developer/CommandLineTools/usr/lib",
|
|
174
|
-
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib",
|
|
175
|
-
"/usr/local/opt/llvm*/lib",
|
|
176
|
-
];
|
|
177
|
-
|
|
178
|
-
/// `libclang` directory patterns for Windows.
|
|
179
|
-
///
|
|
180
|
-
/// The boolean indicates whether the directory pattern should be used when
|
|
181
|
-
/// compiling for an MSVC target environment.
|
|
182
|
-
const DIRECTORIES_WINDOWS: &[(&str, bool)] = &[
|
|
183
|
-
// LLVM + Clang can be installed using Scoop (https://scoop.sh).
|
|
184
|
-
// Other Windows package managers install LLVM + Clang to other listed
|
|
185
|
-
// system-wide directories.
|
|
186
|
-
("C:\\Users\\*\\scoop\\apps\\llvm\\current\\lib", true),
|
|
187
|
-
("C:\\MSYS*\\MinGW*\\lib", false),
|
|
188
|
-
("C:\\Program Files*\\LLVM\\lib", true),
|
|
189
|
-
("C:\\LLVM\\lib", true),
|
|
190
|
-
// LLVM + Clang can be installed as a component of Visual Studio.
|
|
191
|
-
// https://github.com/KyleMayes/clang-sys/issues/121
|
|
192
|
-
("C:\\Program Files*\\Microsoft Visual Studio\\*\\VC\\Tools\\Llvm\\**\\lib", true),
|
|
193
|
-
];
|
|
194
|
-
|
|
195
|
-
/// `libclang` directory patterns for illumos
|
|
196
|
-
const DIRECTORIES_ILLUMOS: &[&str] = &[
|
|
197
|
-
"/opt/ooce/llvm-*/lib",
|
|
198
|
-
"/opt/ooce/clang-*/lib",
|
|
199
|
-
];
|
|
200
|
-
|
|
201
|
-
//================================================
|
|
202
|
-
// Searching
|
|
203
|
-
//================================================
|
|
204
|
-
|
|
205
|
-
/// Finds the files in a directory that match one or more filename glob patterns
|
|
206
|
-
/// and returns the paths to and filenames of those files.
|
|
207
|
-
fn search_directory(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)> {
|
|
208
|
-
// Escape the specified directory in case it contains characters that have
|
|
209
|
-
// special meaning in glob patterns (e.g., `[` or `]`).
|
|
210
|
-
let directory = Pattern::escape(directory.to_str().unwrap());
|
|
211
|
-
let directory = Path::new(&directory);
|
|
212
|
-
|
|
213
|
-
// Join the escaped directory to the filename glob patterns to obtain
|
|
214
|
-
// complete glob patterns for the files being searched for.
|
|
215
|
-
let paths = filenames
|
|
216
|
-
.iter()
|
|
217
|
-
.map(|f| directory.join(f).to_str().unwrap().to_owned());
|
|
218
|
-
|
|
219
|
-
// Prevent wildcards from matching path separators to ensure that the search
|
|
220
|
-
// is limited to the specified directory.
|
|
221
|
-
let mut options = MatchOptions::new();
|
|
222
|
-
options.require_literal_separator = true;
|
|
223
|
-
|
|
224
|
-
paths
|
|
225
|
-
.map(|p| glob::glob_with(&p, options))
|
|
226
|
-
.filter_map(Result::ok)
|
|
227
|
-
.flatten()
|
|
228
|
-
.filter_map(|p| {
|
|
229
|
-
let path = p.ok()?;
|
|
230
|
-
let filename = path.file_name()?.to_str().unwrap();
|
|
231
|
-
|
|
232
|
-
// The `libclang_shared` library has been renamed to `libclang-cpp`
|
|
233
|
-
// in Clang 10. This can cause instances of this library (e.g.,
|
|
234
|
-
// `libclang-cpp.so.10`) to be matched by patterns looking for
|
|
235
|
-
// instances of `libclang`.
|
|
236
|
-
if filename.contains("-cpp.") {
|
|
237
|
-
return None;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
Some((path.parent().unwrap().to_owned(), filename.into()))
|
|
241
|
-
})
|
|
242
|
-
.collect::<Vec<_>>()
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/// Finds the files in a directory (and any relevant sibling directories) that
|
|
246
|
-
/// match one or more filename glob patterns and returns the paths to and
|
|
247
|
-
/// filenames of those files.
|
|
248
|
-
fn search_directories(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)> {
|
|
249
|
-
let mut results = search_directory(directory, filenames);
|
|
250
|
-
|
|
251
|
-
// On Windows, `libclang.dll` is usually found in the LLVM `bin` directory
|
|
252
|
-
// while `libclang.lib` is usually found in the LLVM `lib` directory. To
|
|
253
|
-
// keep things consistent with other platforms, only LLVM `lib` directories
|
|
254
|
-
// are included in the backup search directory globs so we need to search
|
|
255
|
-
// the LLVM `bin` directory here.
|
|
256
|
-
if target_os!("windows") && directory.ends_with("lib") {
|
|
257
|
-
let sibling = directory.parent().unwrap().join("bin");
|
|
258
|
-
results.extend(search_directory(&sibling, filenames));
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
results
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/// Finds the `libclang` static or dynamic libraries matching one or more
|
|
265
|
-
/// filename glob patterns and returns the paths to and filenames of those files.
|
|
266
|
-
pub fn search_libclang_directories(filenames: &[String], variable: &str) -> Vec<(PathBuf, String)> {
|
|
267
|
-
// Search only the path indicated by the relevant environment variable
|
|
268
|
-
// (e.g., `LIBCLANG_PATH`) if it is set.
|
|
269
|
-
if let Ok(path) = env::var(variable).map(|d| Path::new(&d).to_path_buf()) {
|
|
270
|
-
// Check if the path is a matching file.
|
|
271
|
-
if let Some(parent) = path.parent() {
|
|
272
|
-
let filename = path.file_name().unwrap().to_str().unwrap();
|
|
273
|
-
let libraries = search_directories(parent, filenames);
|
|
274
|
-
if libraries.iter().any(|(_, f)| f == filename) {
|
|
275
|
-
return vec![(parent.into(), filename.into())];
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Check if the path is directory containing a matching file.
|
|
280
|
-
return search_directories(&path, filenames);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
let mut found = vec![];
|
|
284
|
-
|
|
285
|
-
// Search the `bin` and `lib` directories in the directory returned by
|
|
286
|
-
// `llvm-config --prefix`.
|
|
287
|
-
if let Some(output) = run_llvm_config(&["--prefix"]) {
|
|
288
|
-
let directory = Path::new(output.lines().next().unwrap()).to_path_buf();
|
|
289
|
-
found.extend(search_directories(&directory.join("bin"), filenames));
|
|
290
|
-
found.extend(search_directories(&directory.join("lib"), filenames));
|
|
291
|
-
found.extend(search_directories(&directory.join("lib64"), filenames));
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Search the toolchain directory in the directory returned by
|
|
295
|
-
// `xcode-select --print-path`.
|
|
296
|
-
if target_os!("macos") {
|
|
297
|
-
if let Some(output) = run_xcode_select(&["--print-path"]) {
|
|
298
|
-
let directory = Path::new(output.lines().next().unwrap()).to_path_buf();
|
|
299
|
-
let directory = directory.join("Toolchains/XcodeDefault.xctoolchain/usr/lib");
|
|
300
|
-
found.extend(search_directories(&directory, filenames));
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Search the directories in the `LD_LIBRARY_PATH` environment variable.
|
|
305
|
-
if let Ok(path) = env::var("LD_LIBRARY_PATH") {
|
|
306
|
-
for directory in env::split_paths(&path) {
|
|
307
|
-
found.extend(search_directories(&directory, filenames));
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Determine the `libclang` directory patterns.
|
|
312
|
-
let directories: Vec<&str> = if target_os!("haiku") {
|
|
313
|
-
DIRECTORIES_HAIKU.into()
|
|
314
|
-
} else if target_os!("linux") || target_os!("freebsd") {
|
|
315
|
-
DIRECTORIES_LINUX.into()
|
|
316
|
-
} else if target_os!("macos") {
|
|
317
|
-
DIRECTORIES_MACOS.into()
|
|
318
|
-
} else if target_os!("windows") {
|
|
319
|
-
let msvc = target_env!("msvc");
|
|
320
|
-
DIRECTORIES_WINDOWS
|
|
321
|
-
.iter()
|
|
322
|
-
.filter(|d| d.1 || !msvc)
|
|
323
|
-
.map(|d| d.0)
|
|
324
|
-
.collect()
|
|
325
|
-
} else if target_os!("illumos") {
|
|
326
|
-
DIRECTORIES_ILLUMOS.into()
|
|
327
|
-
} else {
|
|
328
|
-
vec![]
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
// We use temporary directories when testing the build script so we'll
|
|
332
|
-
// remove the prefixes that make the directories absolute.
|
|
333
|
-
let directories = if test!() {
|
|
334
|
-
directories
|
|
335
|
-
.iter()
|
|
336
|
-
.map(|d| d.strip_prefix('/').or_else(|| d.strip_prefix("C:\\")).unwrap_or(d))
|
|
337
|
-
.collect::<Vec<_>>()
|
|
338
|
-
} else {
|
|
339
|
-
directories
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
// Search the directories provided by the `libclang` directory patterns.
|
|
343
|
-
let mut options = MatchOptions::new();
|
|
344
|
-
options.case_sensitive = false;
|
|
345
|
-
options.require_literal_separator = true;
|
|
346
|
-
for directory in directories.iter() {
|
|
347
|
-
if let Ok(directories) = glob::glob_with(directory, options) {
|
|
348
|
-
for directory in directories.filter_map(Result::ok).filter(|p| p.is_dir()) {
|
|
349
|
-
found.extend(search_directories(&directory, filenames));
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
found
|
|
355
|
-
}
|