How I Avoided Duplicating 70% of My Codebase

A few weeks ago, I decided to write Designite’s extension for Visual Studio. It implies that apart from a desktop and console application, Designite could also be used inside Visual Studio IDE.

The plan went well and I released the extension for Visual Studio 2017; it’s available now on Microsoft marketplace. Further, I wanted to support Visual Studio (VS) 2015 and thought it would a piece of cake. Well… here the fun starts.

I made changes in the same extension to support VS 2015 in addition to VS 2017. It was not a successful attempt. Supporting two different VS editions by a single codebase with same configurations didn’t work for me. I fought with dll hell and went back to older versions of many .NET and Roslyn dlls. Soon I realized it is not a good idea to keep the same codebase and same configurations for two VS editions!

I created a separate solution and copied all the required projects. I fixed the configurations; I set VS 2015 extension to require .NET 4.6 and VS 2017 extension to require .NET 4.7. Further, I updated the dlls based on the need (both has a same/similar set of dlls but with different versions). Now, I had two almost identical codebases with different configurations for two VS extensions.

Both extensions were working properly. However, a new challenge was waiting for me. I don’t want to duplicate 70% of my codebase and maintain two copies. It is not a new problem; software engineers are dealing with this situation from ages especially in software product lines (probably with more elegance). Here is what I tried.

The first thing that I tried is removing existing source files from VS 2015 extension solution and adding the same source files from VS 2017 extension source code (in the hope that the VS 2015 extension projects will refer to the source files in VS 2017 extension solution). It didn’t work since Visual Studio copies the added source files to the current solution (and to the current folder).

In the second attempt, I renamed each project file (.csproj file) in VS 2015 extension solution and corresponding app.config and packages.config, added the new app.config and packages.config names in the corresponding .csproj file manually in the hope that the IDE will refer these newly renamed configuration files when I load the project. I placed these .csproj and *.config (only renamed) in the same folder as the VS 2017 extension configuration files exist. For example, I placed PieChart_.csproj, app_.config, and packages_.config from VS 2015 extension source folder to PieChart project folder where PieChart.csproj, app.config, and packages.config (for VS 2017) are placed. I also changed solution file for VS 2015 to refer to the renamed projects. I was hoping to make it work the two separate solution files for both the extensions (and corresponding project and configuration files) with the same source files. However, to my surprise, Visual Studio changes app.config present in the project folder (and not app_.config). Essentially, configurations of both the extensions are getting mixed.

The third trial was successful in which I created symbolic links in the VS 2015 solution folder. I wrote a python script to delete the source code files (cs and XAML) from all the projects of the VS 2015 extension source. Then I created symbolic links for all the source files present in VS 2017 projects in the VS 2015 projects. This way, I kept the configuration of the two extensions separate while maintaining the single copy of source files.

# This program helps me avoid duplicating visual studio extension source code for VS 2017 and VS 2015
# I delete existing cs files and create a synchronized symbolic links only for the source code files.
# It requires administrative privilege to work.

import os
from subprocess import call

VS2015_PATH = "path to the VS 2015 extension source code"
VS2017_PATH = "path to the VS 2017 extension source code"

def deleteCSFiles():
    for root, dirs, files in os.walk(VS2015_PATH):
        files = [f for f in files if not f[0] == '.']
        dirs[:] = [d for d in dirs if not d[0] == '.']
        for file in files:
            if file.endswith(".cs") | file.endswith(".xaml"):
                os.remove(os.path.join(root, file))

def createSymLinks():
    dirs_to_process = os.listdir(VS2015_PATH)
    for dir in dirs_to_process:
        base_path = os.path.join(VS2017_PATH, dir)
        for root, dirs, files in os.walk(base_path):
            files = [f for f in files if not f[0] == '.']
            dirs[:] = [d for d in dirs if not (d[0] == '.' or d == 'obj' or d == 'bin')]
            for file in files:
                if file.endswith(".cs") | file.endswith(".xaml"):
                    destination_path = os.path.join(root, file)
                    rest_path = destination_path.split(VS2017_PATH)
                    link_path = VS2015_PATH + rest_path[1]
                    if os.path.exists(destination_path):
                        os.system("mklink " + link_path + " " + destination_path)

#-----Start from here-----

There was still a minor roadblock pending to be dealt with. Due to the older versions of dlls used in VS 2015 extension, there were a few compilation errors in VS 2015 solution. To deal with it, I adopted an aged-long technique of conditional compilation. I define a build-tag in one of the VS 2017 extension source code projects and used it in the following way

#if build_tag
//source code

This arrangement allows the source code enclosed in the #if block to compile only in VS 2017 extension solution. That’s it. This is how I avoided duplicating majority of my codebase.