2013-07-01 14 views
10

Sto sviluppando una gemma, che attualmente è puro Ruby, ma ho anche sviluppato una variante C più veloce per una delle funzionalità. La funzione è utilizzabile, ma a volte lenta, in puro Ruby. La lentezza influirebbe solo su alcuni dei potenziali utenti (dipende dalle caratteristiche di cui hanno bisogno e dal modo in cui li usano), quindi ha senso avere la gemma disponibile con un comodo fallback alle sole funzioni di Ruby se non può essere compilata su un sistema di destinazione.Le estensioni native sono di tipo puramente Ruby se non sono supportate sull'installazione gem

Vorrei mantenere le varianti Ruby e C della feature in un singolo gioiello e fornire l'esperienza migliore (ovvero la più veloce) dalla gemma durante l'installazione. Ciò mi permetterebbe di supportare la più ampia serie di potenziali utenti da un mio singolo progetto. Permetterebbe inoltre alle gemme dipendenti e ai progetti di altre persone di utilizzare la migliore dipendenza disponibile su un sistema di destinazione, a differenza di una versione con il minimo comune denominatore per la compatibilità.

mi aspetterei il require di fallback in fase di esecuzione per apparire nel lib/foo.rb file principale semplicemente in questo modo:

begin 
    require 'foo/foo_extended' 
rescue LoadError 
    require 'foo/ext_bits_as_pure_ruby' 
end 

Tuttavia, non so come ottenere l'installazione gemma per controllare (o cercare di fail) per il supporto dell'estensione nativa in modo che la gem si installi correttamente, indipendentemente dal fatto che possa creare "foo_extended". Quando ho cercato come fare, ho trovato principalmente discussioni di alcuni anni fa, ad es. http://permalink.gmane.org/gmane.comp.lang.ruby.gems.devel/1479 e http://rubyforge.org/pipermail/rubygems-developers/2007-November/003220.html che implicano che le gemme Ruby non supportano realmente questa funzione. Niente di recente però, quindi spero che qualcuno su SO abbia delle conoscenze più aggiornate?

La mia soluzione ideale sarebbe un modo per rilevare, prima di tentare una build dell'estensione, che il Ruby di destinazione non supportasse (o forse semplicemente non volesse, a livello di progetto) estensioni native C. Ma anche un meccanismo try/catch sarebbe OK se non troppo sporco.

E 'possibile, se sì, come? Oppure il consiglio di pubblicare due varianti di gemme (ad esempio foo e foo_ruby), che sto riscontrando quando cerco, le best practice ancora correnti?

+0

Due gemme vanno bene, ad es. la gemma json è disponibile in due varianti: ['json'] (https://rubygems.org/gems/json) (con estensione C) e [' json_pure'] (https://rubygems.org/gems/json_pure) (puro rubino). – Stefan

+0

@Stefan: Nella conversazione che ho collegato, l'autore/maintaner di 'json' e' json_pure' a quanto pare preferirebbe diversamente. Oltre al lavoro extra che pubblica due varianti della gemma, i progetti dipendenti che possono essere essi stessi dipendenze di qualcos'altro, finiscono per dover usare il minimo comune denominatore o devono anche fornire due varianti solo per gestire le dipendenze che non codificano . Non direi che "va bene", ma se è il migliore possibile, allora è tutto quello che posso fare anch'io –

+0

@Stefan: Sicuramente l'intenzione progettuale di Neil incarnata nell'OP è giusta. La sua domanda è grande e molto importante, sono interessato alla risposta, per favore, per favore. –

risposta

1

Questo è il mio miglior risultato il tentativo di rispondere alla mia domanda fino ad oggi. Sembra funzionare per JRuby (testato su Travis e sulla mia installazione locale sotto RVM), che era il mio obiettivo principale. Tuttavia, sarei molto interessato a conferme di IT che lavorano in altri ambienti, e per qualsiasi input su come renderlo più generico e/o robusto:


Il codice di installazione gemma aspetta una Makefile come uscita da extconf.rb , ma non ha un'opinione su cosa dovrebbe contenere.Pertanto, extconf.rb può decidere di creare un numeroMakefileanziché chiamare create_makefile da mkmf. In pratica che potrebbe essere simile a questo:

ext/foo/extconf.rb

can_compile_extensions = false 
want_extensions = true 

begin 
    require 'mkmf' 
    can_compile_extensions = true 
rescue Exception 
    # This will appear only in verbose mode. 
    $stderr.puts "Could not require 'mkmf'. Not fatal, the extensions are optional." 
end 


if can_compile_extensions && want_extensions 
    create_makefile('foo/foo') 

else 
    # Create a dummy Makefile, to satisfy Gem::Installer#install 
    mfile = open("Makefile", "wb") 
    mfile.puts '.PHONY: install' 
    mfile.puts 'install:' 
    mfile.puts "\t" + '@echo "Extensions not installed, falling back to pure Ruby version."' 
    mfile.close 

end 

Come suggerito nella domanda, questa risposta richiede anche la seguente logica per caricare il codice fallback Ruby nella principale biblioteca:

lib/foo.rb (estratto)

begin 
    # Extension target, might not exist on some installations 
    require 'foo/foo' 
rescue LoadError 
    # Pure Ruby fallback, should cover all methods that are otherwise in extension 
    require 'foo/foo_pure_ruby' 
end 

Seguire questa rotta richiede anche qualche giocoleria delle attività di rake, in modo che l'attività di rake predefinita non tenti di compilare su Rubies su cui stiamo testando che non hanno la possibilità di compilare estensioni:

Rakefile (estratti)

def can_compile_extensions 
    return false if RUBY_DESCRIPTION =~ /jruby/ 
    return true 
end 

if can_compile_extensions 
    task :default => [:compile, :test] 
else 
    task :default => [:test] 
end 

Nota la parte Rakefile non deve essere del tutto generico, ha solo per coprire ambienti noti che vogliamo costruire e testare a livello locale la gemma su (ad esempio, tutti gli obiettivi di Travis).

Ho notato un fastidio. Per impostazione predefinita, verrà visualizzato il messaggio Ruby Gems Building native extensions. This could take a while... e nessuna indicazione che la compilazione dell'estensione sia stata saltata. Tuttavia, se invochi l'installer con gem install foo --verbose, vedi i messaggi aggiunti a extconf.rb, quindi non è male.

1

Ecco un pensiero, basato su informazioni da http://guides.rubygems.org/c-extensions/ e http://yorickpeterse.com/articles/hacking-extconf-rb/.

Sembra che sia possibile inserire la logica in extconf.rb. Ad esempio, interrogare la costante RUBY_DESCRIPTION e determinare se si è in un rubino che supporta le estensioni native:

$ irb 
jruby-1.6.8 :001 > RUBY_DESCRIPTION 
=> "jruby 1.6.8 (ruby-1.8.7-p357) (2012-09-18 1772b40) (Java HotSpot(TM) 64-Bit Server VM  
    1.6.0_51) [darwin-x86_64-java]" 

Così si potrebbe provare qualcosa di simile a racchiudere il codice in extconf.rb in un condizionale (in extconf.rb):

unless RUBY_DESCRIPTION =~ /jruby/ do 

    require 'mkmf' 

    # stuff  
    create_makefile('my_extension/my_extension') 

end 

Ovviamente, si vuole la logica più sofisticata, afferrando parametri passati su "gem install", ecc

+0

Questo codice in 'extconf.rb' non ha funzionato. Ma un codice simile nella gemspec attorno alla dipendenza da rake-install e che dichiara che l'estensione sembra funzionare come richiesto. Ho bisogno di giocarci un po 'di più, ma è promettente –

+0

La modifica di gemspec mi ha permesso di testare nuovamente la gemma su JRuby, e ho avuto tutte le mie attività di rake in tutti i rubini di prova, ma alla fine non ha funzionato neanche. Il problema è che '.gemspec' viene elaborato troppo presto, quindi funziona solo quando la build della gemma viene eseguita nello stesso target Ruby. Comunque 'extconf.rb' viene elaborato troppo tardi, se la gemma contiene' extconf.rb' il sistema di destinazione può rifiutarla prima che venga eseguita qualsiasi logica in essa contenuta. –

+0

Grazie per i suggerimenti, hanno portato a quello che penso sia una risposta valida. Tuttavia, c'è molto di più che rilevare gli obiettivi che non si compilano, quindi ho aggiunto le mie scoperte come una nuova risposta. –

Problemi correlati