Monday, July 30, 2012

Gmake Acrobatics

Lets start with obvious. Suppose you have in your Makefile several variables:

DB_HOST := 127.0.0.1
DB_PORT := 5432
DB_USER := joe
DB_NAME := test

Variable values come from some configuration file outside of this Makefile. There is no point of replication such information & holding it in 2 places (config & Makefile). So you start thinking like this: "I'll just read my config file from my Makefile and assign variables dynamically. That's easy."

Suppose the config is in json format. Using handy jsontool we can write:

  DB_HOST := $(shell json db.host < myconfig.json)  

Okay. But with this approach we need to execute jsontool every time for each variable. For n variable this will be exactly n forks. Suddenly every task in your Makefile becomes a little (or not a little) sluggish.

It is possible, of course, to execute jsontool only once and get a newline separated 'list' of all values:

$ json db.host db.port db.user db.name < myconfig.json
127.0.0.1
5432
joe
test

But how do you map those into Makefile variables?

In every other language this would be very simple: iterate over a list of variable names; for each name, construct a string var := value and feed it to eval function. Fot example, in lovely CoffeeScript:

values = ['a', 'b']
MyEval "#{v} := #{values[count++]}" for v,count in ['DB_HOST', 'DB_PORT']  

Try to translate this into gmake code & you will struggle badly. gmake can iterate on a string, splitting it with spaces. It has eval function which can parse & evaluate its makefile language. It even has some simple helper functions for manipulating strings, for example $(word 2,this is nice) would return the word 'is'.

What is doesn't have is basic arithmetic support. You can't add 1 + 1 in it without executing a shell script or whatever.

Googling may bring up this link to you, which is hilarious in its very own way but has a great idea: if we need a simple counter, we can start it with an empty string (yes, string), call gmake function $(words $(string)) which will return 0. Then we concatenate the string with another string containing a space & a char (for example, ' x'), call function $(words $(string)) again & it will return 1. And so on.

This is what I ended with:

# Create a batch of variables on-the-fly.
# _left contains variable names, _right--their corresponding values.
_left := DB_NAME DB_USER DB_HOST DB_PORT
_right := $(shell json db_name db.user db.host db.port < myconfig.json)
_n := 1
define _dvars
$(i) := $$(word $$(words $$(_n)),$$(_right))
_n := $$(_n) x
endef
$(foreach i,$(_left),$(eval $(_dvars)))

It works but looks ugly. Those define...endif construction is just a way to define a variable that contains newlines. eval function evaluates it twice for every iteration. This is why we need $$ in front of every dynamic construction except of $(i) parameter.

Hint: user Rake & don't waste your time.

Monday, June 18, 2012

On-the-fly Generator of Preferences Pages for Opera Extensions

If you've ever tried to write an Opera extension, then you probably have stumbled upon a process of handling preferences for your extensions.

When a user clinks 'Preferences' in an extension menu button, Opera reads options.html file from the installed extension. What goes to that options.html is up to the developer. Nothing prevents him to display a lolcat video instead of html forms.

The process of writing options.html if everything except creative--it's the same boring crap over & over again for every new extension. I don't get why you even have to do this--Opera could have an API to help automatically generate preferences pages like it have it internally for the browser (opera:config). But there isn't API for this & nobody pushes for it.

So image you're writing in a declarative way what preferences your extension needs & the browser is drawing GUI elements according to your specification. Not code, just a declaration.

If you agree to this approach & don't want to waste your time on a dumb & repetitive staff, see weakspec on-the fly generator. You'll probably like it.

Friday, April 13, 2012

How To Disable Rack Logging in Sinatra

To completely & finally turn the annoying Rack logging off, simple 'set :logging, nil' doesn't help. Instead, insert this monkey patch at the beginning of your Sinatra application:

# Disable useless rack logger completely! Yay, yay!
module Rack
  class CommonLogger
    def call(env)
      # do nothing
      @app.call(env)
    end
  end
end

Custom RDoc's Darkfish Templates

RDoc 3.12 allows us to specify a template for a formatter. Formatter can be 'ri' or such that emits html. The later is called 'darkfish' in RDoc.

The problem with darkfish is that albeit it contains a quite nice navigation, it hurts my eyes:

https://lh5.googleusercontent.com/-g62JInQnNmc/T4dJdslbgQI/AAAAAAAAAXE/jZY55_VNhoo/s800/rdoc-template.png

Dark grey on light grey! Very artistic choice, of course. I believe it's very possible to invent even worse combination, like red on green, but I still don't get how anybody can like the absence of a contrast.

Anyway, here is a solution: another template for darkfish. (Not another formatter.)

RDoc allows that if you install another template as a gem, because it looks for templates only in rdoc/generator/template directory in Ruby's $LOAD_PATH.

What if you want to generate alternate looking html from a particular Rakefile without messing up with system gems?

  1. Copy the original template to some editable place, for example:

    % cp /usr/[...]/1.9/gems/rdoc-3.12/lib/rdoc/generator/template/darkfish \
    /home/alex/Desktop/lightfish
    

    'lightfish' is out new template in this example.

  2. Edit lightfish/rdoc.css to remove ugly colors, fonts, etc.

  3. Add a small monkey patch to your project's Rakefile:

    class RDoc::Options
      def template_dir_for template
        '/home/alex/Desktop/' + template
      end
    end
    
    RDoc::Task.new('html') do |i|
      i.template = 'lightfish'
      i.main = 'README.rdoc'
      i.rdoc_files = FileList['doc/*', 'lib/**/*.rb', '*.rb']
    end
    

    template_dir_for() function is a key to succsess.

  4. Run rake html. RDoc must not complain about 'could not find template lightfish'.

But there is even simpler method with $LOAD_PATH. Make rdoc/generator/template directory somewhere, for example in the project's root directory. Move in it a modified template from steps 1-2 above and just run (assuming rake's target for generating documentation is still called 'html'):

% rake -I . html

Friday, January 27, 2012

Rubygems rdoc spec exclude & rdoc task exclude

One more or less popular error in gemspec:

spec = Gem::Specification.new {|i|
  [...]

  i.rdoc_options << '-m' << 'doc/README.rdoc' << '-x' << 'lib/**/templates/**/*'
  i.extra_rdoc_files = FileList['doc/*']

  [...]
}

In this example a pattern after -x option is invalid. rdoc internally constructs an Regexp object from argument to -x option:

  • If you supply a string 'foo' it will end up as /foo/ argument to a Regexp.new.
  • If you have several -x options like -x foo -x bar, the argument will be /foo|bar/.

(At first rdoc constructs an array from all -x options and then converts it into 1 regexp.)

Now, 'lib/**/templates/**/*' is obviously a bogus regexp and rdoc will loudly complain about it during gem installation. [1] How did it appear in the spec in the first place?

A developer just copied a string from RDoc task which might look like:

RDoc::Task.new('html') do |i|
      i.main = "doc/README.rdoc"
      i.rdoc_files = FileList['doc/*', 'lib/**/*.rb']
      i.rdoc_files.exclude 'lib/**/templates/**/*'
end

This rake task works as intended: 'lib/**/templates/**/*' parameter is a valid one because i.rdoc_files is a Rake::FileList object and exclude method of that object expects such kind of patterns and doesn't understand regexps.

So, don't mix up those patterns it the spec and in the RDoc task itself.

[1]The right one would be something like 'lib/.+/templates/'.