Module: FlossFunding

Defined in:
lib/floss_funding/wedge.rb,
lib/floss_funding/poke.rb,
lib/floss_funding/config.rb,
lib/floss_funding/silent.rb,
lib/floss_funding/library.rb,
lib/floss_funding/version.rb,
lib/floss_funding/lockfile.rb,
lib/floss_funding/constants.rb,
lib/floss_funding/inclusion.rb,
lib/floss_funding/namespace.rb,
lib/floss_funding/under_bar.rb,
lib/floss_funding/validators.rb,
lib/floss_funding/file_finder.rb,
lib/floss_funding/fingerprint.rb,
lib/floss_funding/rake_helpers.rb,
lib/floss_funding/config_finder.rb,
lib/floss_funding/config_loader.rb,
lib/floss_funding/configuration.rb,
lib/floss_funding/final_summary.rb,
lib/floss_funding/activation_event.rb,
lib/floss_funding/terminal_helpers.rb,
lib/floss_funding/contra_indications.rb,
lib/floss_funding/rakelib/gem_spec_reader.rb,
lib/floss_funding.rb

Overview

Now declare some constants

Defined Under Namespace

Modules: Config, Constants, FileFinder, Fingerprint, Lockfile, Poke, RakeHelpers, Rakelib, Silent, Terminal, UnderBar, Validators, Version Classes: ActivationEvent, AtExitLockfile, ConfigFinder, ConfigLoader, ConfigNotFoundError, Configuration, ContraIndications, Error, FinalSummary, Inclusion, Library, LockfileBase, Namespace, OnLoadLockfile, Wedge

Constant Summary collapse

DEBUG =

Debug toggle controlled by ENV; set true when ENV[‘FLOSS_CFG_FUND_DEBUG’] case-insensitively equals “true”.

begin
  v = ENV.fetch("FLOSS_CFG_FUND_DEBUG", nil)
  v.to_s.casecmp("true") == 0
rescue StandardError
  false
end
CONFIG_FILE_NAME =

The file name to look for in the project root.

Returns:

  • (String)
".floss_funding.yml"
FF_ROOT =
File.realpath(File.join(File.dirname(__FILE__), ".."))
REQUIRED_YAML_KEYS =

Minimum required keys for a valid .floss_funding.yml file
Used to validate presence when integrating without :wedge mode

%w[library_name funding_uri].freeze
FREE_AS_IN_BEER =

Unpaid activation option intended for open-source and not-for-profit use.

Returns:

  • (String)
"Free-as-in-beer"
BUSINESS_IS_NOT_GOOD_YET =

Unpaid activation option acknowledging commercial use without payment.

Returns:

  • (String)
"Business-is-not-good-yet"
NOT_FINANCIALLY_SUPPORTING =

Activation option to explicitly opt out of funding prompts for a namespace.

Returns:

  • (String)
"Not-financially-supporting"
STATES =
{
  :activated => "activated",
  :unactivated => "unactivated",
  :invalid => "invalid",
  :detained => "detained",
}.freeze
STATE_VALUES =
STATES.values.freeze
DEFAULT_STATE =

The default state is unknown / unactivated until proven otherwise.

STATES[:unactivated]
START_MONTH =

First month index against which base words are validated.
Do not change once released as it would invalidate existing activation keys.

Returns:

  • (Integer)
Month.new(2025, 7).to_i
BASE_WORDS_PATH =

Absolute path to the base words list used for paid activation validation.

Returns:

  • (String)
File.expand_path("../../base.txt", __FILE__)
EIGHT_BYTES =

Number of hex characters required for a paid activation key (64 = 256 bits).

Returns:

  • (Integer)
64
HEX_LICENSE_RULE =

Format for a paid activation key (64 hex chars).

Returns:

  • (Regexp)
/\A[0-9a-fA-F]{#{EIGHT_BYTES}}\z/
<<-FOOTER
=====================================================================================
- Please buy FLOSS "peace-of-mind" activation keys to support open source developers.
floss_funding v#{::FlossFunding::Version::VERSION} is made with ❤️ in 🇺🇸 & 🇮🇩 by Galtzo FLOSS (galtzo.com)
FOOTER

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.loaded_at ⇒ Time (readonly)

Read the deterministic time source

Returns:

  • (Time)

See Also:

  • @loaded_at


253
254
255
# File 'lib/floss_funding.rb', line 253

def loaded_at
  @loaded_at
end

Class Method Details

.add_or_update_namespace_with_event(namespace, event) ⇒ Object



394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/floss_funding.rb', line 394

def add_or_update_namespace_with_event(namespace, event)
  @mutex.synchronize do
    ns_obj = namespace
    # Append in place to reduce allocations and avoid extra @mutex churn
    ns_obj.activation_events << event
    @namespaces[namespace.name] = ns_obj
    begin
      lib_name = (event.library ? event.library.library_name : nil)
      ::FlossFunding.debug_log { "[registry] add_or_update ns=#{namespace.name.inspect} events=#{ns_obj.activation_events.size} state=#{event.state} lib=#{lib_name.inspect}" }
    rescue StandardError
      # ignore log errors
    end
  end
end

.all_namespaces ⇒ Array<::FlossFunding::Namespace>

All namespaces that have any activation events recorded
Returns array of Namespace objects

Returns:



412
413
414
# File 'lib/floss_funding.rb', line 412

def all_namespaces
  @mutex.synchronize { @namespaces.values.flatten.dup }
end

.base_words(num_valid_words = nil) ⇒ Array<String>

Reads the first N lines from the base words file to validate paid activation keys.

Reads base words used to validate paid activation keys.
When called without an argument, uses the current month window to
determine how many words are valid.

Parameters:

  • num_valid_words (Integer, nil) (defaults to: nil) —

    number of words to read from the word list

Returns:

  • (Array<String>) —

    the first N words; empty when N is nil or zero



451
452
453
454
455
456
# File 'lib/floss_funding.rb', line 451

def base_words(num_valid_words = nil)
  n = num_valid_words.nil? ? @num_valid_words_for_month : num_valid_words
  return [] if n.nil? || n.zero?

  @base_words_all.slice(0, n)
end

.check_activation(plain_text) ⇒ Boolean

Check whether a plaintext activation base word is currently valid

Parameters:

  • plain_text (String)

Returns:

  • (Boolean)


461
462
463
464
465
466
467
468
469
470
471
472
473
474
# File 'lib/floss_funding.rb', line 461

def check_activation(plain_text)
  return false if @num_valid_words_for_month.nil? || @num_valid_words_for_month <= 0
  # Cache a Set for fast membership per current n
  sets = @base_words_set_cache
  set = sets[@num_valid_words_for_month]
  unless set
    words = base_words(@num_valid_words_for_month)
    # Warning inside a cache-protected lookup means it should only happen once per process, at most.
    warn("[FlossFunding] ZOMG! Base words missing. Did you time travel? Is it #{@loaded_month}? Is system clock set in the past?") if words.empty?
    set = Set.new(words)
    sets[@num_valid_words_for_month] = set
  end
  set.include?(plain_text)
end

.configuration(namespace) ⇒ Object



432
433
434
# File 'lib/floss_funding.rb', line 432

def configuration(namespace)
  configurations(namespace)
end

.configurations(namespace = nil) ⇒ Object

Configuration storage and helpers (derived from namespaces and activation events)
When namespace is nil, returns a Hash mapping namespace String => Array<FlossFunding::Configuration>.
When namespace is provided, returns FlossFunding::Configuration for that namespace (or nil if not found).



419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/floss_funding.rb', line 419

def configurations(namespace = nil)
  @mutex.synchronize do
    if namespace
      nobj = @namespaces[namespace]
      nobj ? nobj.merged_config : nil
    else
      @namespaces.each_with_object({}) do |(ns, nobj), acc|
        acc[ns] = nobj.configs
      end
    end
  end
end

.debug_log(*args) ⇒ void

This method returns an undefined value.

Debug logging helper. Only outputs when FlossFunding::DEBUG is true.
Accepts either a message (or multiple args joined by space) or a block
for lazy construction of the message.

Parameters:

  • args (Array<Object>) —

    message parts to join with space

Yield Returns:

  • (String) —

    optional block returning the message



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/floss_funding.rb', line 272

def debug_log(*args)
  return unless ::FlossFunding::DEBUG
  msg = if block_given?
    yield
  else
    args.map(&:to_s).join(" ")
  end
  # Prefer Logger to file when configured and available; otherwise STDOUT
  logger = debug_logger
  if logger
    begin
      logger.debug(msg.to_s)
      return
    rescue StandardError
      # fall back to STDOUT below
    end
  end
  puts(msg)
rescue StandardError
  # Never fail the caller due to logging issues
  nil
end

.debug_logger ⇒ Object

Lazily build a Logger instance when FLOSS_CFG_FUNDING_LOGFILE is set and ‘logger’ is available.
Returns a Logger or nil when unavailable or initialization failed.



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/floss_funding.rb', line 320

def debug_logger
  path = begin
    ENV["FLOSS_CFG_FUNDING_LOGFILE"]
  rescue StandardError
    nil
  end
  return if path.nil? || path.to_s.strip.empty?

  begin
    require "logger"
  rescue LoadError
    return
  rescue StandardError => e
    # Log but do not set inert for logger init failures
    debug_log { "[WARN][debug_logger] #{e.class}: #{e.message}" }
    return
  end

  @mutex.synchronize do
    return @debug_logger if defined?(@debug_logger) && @debug_logger

    # Ensure directory exists; best-effort
    begin
      dir = File.dirname(path)
      FileUtils.mkdir_p(dir) unless dir.nil? || dir.empty? || Dir.exist?(dir)
    rescue StandardError
      # ignore; Logger.new may still succeed if dir already exists or is current dir
    end

    begin
      # Truncate the debug log file on first initialization to keep runs readable
      begin
        File.open(path, "w") { |f| f.truncate(0) }
      rescue StandardError => e
        debug_log { "[WARN][debug_logger] unable to truncate #{path}: #{e.class}: #{e.message}" }
      end

      logger = Logger.new(path)
      logger.level = Logger::DEBUG
      # Keep output minimal: message only with newline
      logger.formatter = proc { |_severity, _datetime, _progname, message| (message.to_s.end_with?("\n") ? message.to_s : message.to_s + "\n") }
      @debug_logger = logger
    rescue StandardError
      @debug_logger = nil
    end

    @debug_logger
  end
end

.env_var_names ⇒ Object

ENV var name mapping helpers (derived from namespaces)
Returns a Hash mapping namespace String => ENV variable name String



438
439
440
441
442
# File 'lib/floss_funding.rb', line 438

def env_var_names
  @mutex.synchronize do
    @namespaces.transform_values { |nobj| nobj.env_var_name }
  end
end

.error!(error, where = nil) ⇒ Object

Mark an internal error, log useful context for diagnostics, and set inert flag.

Parameters:

  • error (Exception)
  • where (String, nil) (defaults to: nil) —

    context label



303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/floss_funding.rb', line 303

def error!(error, where = nil)
  begin
    lbl = where ? "[ERROR][#{where}]" : "[ERROR]"
    msg = "#{lbl} #{error.class}: #{error.message}"
    debug_log { msg }
    bt = (error.backtrace || [])[0, 5].join("\n")
    debug_log { "#{lbl} backtrace:\n#{bt}" } unless bt.empty?
  rescue StandardError
    # ignore logging failures
  ensure
    @mutex.synchronize { @errored = true }
  end
  true
end

.errored? ⇒ Boolean

Global error flag; when set true, library should become inert.

Returns:

  • (Boolean)


296
297
298
# File 'lib/floss_funding.rb', line 296

def errored?
  @mutex.synchronize { !!@errored }
end

.initiate_begging(event) ⇒ Object



515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/floss_funding.rb', line 515

def initiate_begging(event)
  library = event.library
  ns = library.namespace
  env_var_name = ::FlossFunding::UnderBar.env_variable_name(ns)
  library_name = library.library_name
  activation_key = event.activation_key

  # On-load nag sentinel: allow each library to nag at most once per lockfile lifetime
  lock = ::FlossFunding::Lockfile.on_load

  case event.state
  when ::FlossFunding::STATES[:activated]
    nil
  when ::FlossFunding::STATES[:invalid]
    unless lock && lock.nagged?(library)
      lock.record_nag(library, event, "on_load") if lock
      ::FlossFunding.start_coughing(activation_key, ns, env_var_name)
    end
  when ::FlossFunding::STATES[:detained]
    unless lock && lock.nagged?(library)
      lock.record_nag(library, event, "on_load") if lock
      puts %(FLOSS Funding: Configuration for #{library_name} (#{ns}) contains invalid values and has been detained; details will be shown at exit.) unless ::FlossFunding::ContraIndications.at_exit_contraindicated?
    end
  else
    unless lock && lock.nagged?(library)
      lock.record_nag(library, event, "on_load") if lock
      ::FlossFunding.start_begging(ns, env_var_name, library_name)
    end
  end
end

.install_tasks ⇒ Object

Tasks for both development and test environments



262
263
264
# File 'lib/floss_funding.rb', line 262

def install_tasks
  load("floss_funding/tasks.rb")
end

.namespaces ⇒ Hash{String => ::FlossFunding::Namespace}

Accessor for namespaces hash: keys are namespace strings, values are Namespace objects

Returns:



372
373
374
# File 'lib/floss_funding.rb', line 372

def namespaces
  @mutex.synchronize { @namespaces.dup }
end

.namespaces=(value) ⇒ Object

Replace the namespaces hash (expects Hash{String => ::FlossFunding::Namespace})



377
378
379
# File 'lib/floss_funding.rb', line 377

def namespaces=(value)
  @mutex.synchronize { @namespaces = value }
end

.progress_bar(activated_count, total_count) ⇒ Object

Display a concise progress indicator for activated vs total libraries.
Falls back gracefully if ruby-progressbar is not available or when total is zero.

  • In TTY: renders a progress bar and ensures a trailing newline.
  • In non-TTY: prints a stable summary with counts to avoid spinner artifacts.

Parameters:

  • activated_count (Integer)
  • total_count (Integer)


161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/floss_funding.rb', line 161

def progress_bar(activated_count, total_count)
  begin
    require "ruby-progressbar"
  rescue LoadError
    # Fallback without ruby-progressbar: print a concise summary with percentage and counts
    total_i = total_count.to_i
    if total_i <= 0
      puts "FUNDED🦷%: 0% (0/0)"
    else
      pct = ((activated_count.to_f / total_i.to_f) * 100).round
      puts "FUNDED🦷%: #{pct}% (#{activated_count}/#{total_i})"
    end
    return
  end

  total = [total_count, 0].max
  activated = [[activated_count, 0].max, total].min
  if total.zero?
    # Avoid creating a progressbar with zero total; print a stable fallback
    puts "FUNDED🦷%: 0% (0/0)"
    return
  end

  bar = ProgressBar.create(:title => "FUNDED🦷%", :total => total, :format => "%t: |%B| %p%% (%c/%C)")
  bar.progress = activated
  # Ensure we end with a newline after progress bar output without forcing completion
  if $stdout.tty?
    puts ""
  else
    # In non-TTY (e.g., CI capture), provide a stable summary with counts
    puts "(#{activated}/#{total})"
  end
end

.project_root ⇒ String?

Expose the discovered project root (may be nil when running inside this gem’s own repo)

Returns:

  • (String, nil)


257
258
259
# File 'lib/floss_funding.rb', line 257

def project_root
  ::FlossFunding::ConfigFinder.project_root
end

.register_wedge(base, custom_namespace = nil, contraindicated = false) ⇒ Object

Register a minimal activation event for wedge-injected libraries to ensure
they are counted in the final summary without performing config discovery.

Parameters:

  • base (Module) —

    the including module

  • custom_namespace (String, nil) (defaults to: nil) —

    optional override namespace



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/floss_funding.rb', line 199

def register_wedge(base, custom_namespace = nil, contraindicated = false)
  # Derive namespace string
  ns_name = (custom_namespace.is_a?(String) && custom_namespace.strip != "") ? custom_namespace : base.name.to_s

  # Build Namespace (derives activation key/state from ENV)
  namespace = ::FlossFunding::Namespace.new(ns_name, base)

  # Derive a library (gem) name from the namespace: underscore segments and downcase
  # Example: "FlossFunding" => "floss_funding"; "My::Lib" => "my_lib"
  derived_lib_name = ns_name.split("::").map { |seg| ::FlossFunding::UnderBar.to_under_bar(seg) }.join("__").downcase

  # Minimal configuration: include required keys so downstream consumers have something sensible
  cfg_hash = {
    "library_name" => ["wedge_#{derived_lib_name}"],
    "funding_uri" => ["https://floss-funding.dev"],
  }
  config = ::FlossFunding::Configuration.new(cfg_hash)

  # Minimal Library record; many fields are nil or placeholders in wedge mode
  library = ::FlossFunding::Library.new(
    derived_lib_name,        # library_name
    namespace,               # ns
    custom_namespace,        # custom_ns
    base.name.to_s,          # base_name
    nil,                     # including_path
    nil,                     # root_path
    nil,                     # config_path
    namespace.env_var_name,  # env_var_name
    config,                  # configuration
    nil,                     # silent
  )

  # Event with the derived state and key
  event = ::FlossFunding::ActivationEvent.new(
    library,
    namespace.activation_key,
    namespace.state,
    nil,
  )

  add_or_update_namespace_with_event(namespace, event)
  initiate_begging(event) unless contraindicated

  event
rescue StandardError => e
  # Never raise; wedge registration is best-effort only
  ::FlossFunding.error!(e, "register_wedge")
  nil
end

.silenced ⇒ Boolean

Global silenced flag accessor (boolean)

Returns:

  • (Boolean)


383
384
385
# File 'lib/floss_funding.rb', line 383

def silenced
  @mutex.synchronize { @silenced }
end

.silenced=(value) ⇒ void

This method returns an undefined value.

Set the global silenced flag

Parameters:

  • value (Boolean)


390
391
392
# File 'lib/floss_funding.rb', line 390

def silenced=(value)
  @mutex.synchronize { @silenced = !!value }
end

.start_begging(namespace, env_var_name, library_name) ⇒ void

This method returns an undefined value.

Emit the standard friendly funding message for unactivated usage

Parameters:

  • namespace (String)
  • env_var_name (String)
  • library_name (String)


510
511
512
513
# File 'lib/floss_funding.rb', line 510

def start_begging(namespace, env_var_name, library_name)
  return if ::FlossFunding::ContraIndications.at_exit_contraindicated?
  puts %(FLOSS Funding: Activation key missing for #{library_name} (#{namespace}). Set ENV["#{env_var_name}"] to your activation key; details will be shown at exit.)
end

.start_coughing(activation_key, namespace, env_var_name) ⇒ void

This method returns an undefined value.

Emit a diagnostic message when an activation key is invalid

Parameters:

  • activation_key (String)
  • namespace (String)
  • env_var_name (String)


481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# File 'lib/floss_funding.rb', line 481

def start_coughing(activation_key, namespace, env_var_name)
  return if ::FlossFunding::ContraIndications.at_exit_contraindicated?
  puts <<-COUGHING
==============================================================
COUGH, COUGH.
Ahem, it appears as though you tried to set an activation key
for #{namespace}, but it was invalid.

  Current (Invalid) Activation Key: #{activation_key}
  Namespace: #{namespace}
  ENV Variable: #{env_var_name}

Paid activation keys are 8 bytes, 64 hex characters, long.
Unpaid activation keys have varying lengths, depending on type and namespace.
Yours is #{activation_key.length} characters long, and doesn't match any paid or unpaid keys.

Please unset the current ENV variable #{env_var_name}, since it is invalid.

Then find the correct one, or get a new one @ https://floss-funding.dev and set it.

#{FlossFunding::FOOTER}
  COUGHING
end