There is no one true way.
However, Puppetlabs recommends, and Puppeteers around here seem generally to agree, that configurations managed by Puppet benefit greatly from being broken down into reusable modules that can be shared across all nodes that require them.
OS-specific details are typically accounted for within such modules, instead of at a higher level such as your alternatives (2) and (4) suggest. You should consider looking at a few modules from the module forge for examples of how this can be done. Note also that Puppet modules often can be written so that most or even all manifest code can be shared across operating systems -- especially across different Unix operating systems.
John