Anyhow, I'm curious to know everyone else's approach to compatibility, as this is rather a new realm for me.
I use a 2 step mechanism, a coarse one bases on dynamic library selection, and a fine one based on late binding. This works because of a layered solution of the graphics sub-system.
First of, somewhere in the starting process of the application, a OpenGL context is created with the highest supported version. If this fails then a context with the next lower version is tried until the lowest supported version. This can be done in a comfortable way on Mac OS, because it gives one the highest available version when asked for a 3.2 core profile anyway. On Windows, the process is less comfortable. However, if a context was generated, the belonging dynamic library is loaded. If this fails, then again the next lower version for context creation is tried. In the end I hope to have a library successfully loaded, or else the application is not runnable, of course.
Such a library brings in the lowest layer of the graphics sub-system w.r.t. the own implementation. It is able to process graphics jobs that are generated within the middle layer of the graphics sub-system. However, when a library was loaded successfully it has a defined minimum of features. Additional features or better implementations are then recognized by the set-up routine of the graphics sub-system layer loaded with the library. This is based on investigating the minor version number and the extensions. The layer then does symbol binding by its own.
Regarding shader scripts: Some scripts are assembled at the runtime, and this considers to use the mechanisms available from the loaded library. Pre-made scripts need to be available for each loadable library, of course, and hence are simply selected.
EDIT: BTW: The above mechanism is not only used for version compatibility but is abstract enough to also select between OpenGL and D3D.