Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

documentation: how to compile ScottPlot5 using old build tools #2233

Closed
swharden opened this issue Nov 6, 2022 · 1 comment
Closed

documentation: how to compile ScottPlot5 using old build tools #2233

swharden opened this issue Nov 6, 2022 · 1 comment

Comments

@swharden
Copy link
Member

swharden commented Nov 6, 2022

Thanks Lennox808 for sharing this information on Discord (#1966)

Here is the faq/environment/index.md file that now describes the workflow to make the ScottPlot5 solution compilable (except ScottPlot.Tests)

---
title: Development Environment Setup - ScottPlot FAQ
description: How to Build ScottPlot from Source Code using Visual Studio
---

## Build ScottPlot from Source

_Building from source lets developers modify the ScottPlot code to customize behavior, fix bugs, and add new features. These steps are the easiest way for new contributors to get up and running with ScottPlot._

**Step 1:** Install the latest version of [Visual Studio Community](https://visualstudio.microsoft.com/vs/community/) (free). During installation indicate you wish to install the ".NET desktop development" workload.

**Step 2:** Download ScottPlot code from https://github.com/scottplot/scottplot

**Step 3:** Open the `.sln` file in the `src` folder in Visual Studio

**Step 4:** Right-click one of the demo projects and "Set as Startup Project"

**Step 5:** Press F5 to Build and Run 🚀

> 💡 If the build fails with an error message, read it carefully. It may indicate you need a particular version of the [.NET SDK SDK](https://dotnet.microsoft.com/en-us/download/visual-studio-sdks) or [.NET Framework Developer Pack](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net48) installed on your system. You may need to restart Visual Studio after installing these tools.

## Building with Older Tools

**ScottPlot uses modern C# language features** like [file-scoped namespaces](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/file-scoped-namespaces), [lambda expressions](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions), [global using directives](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive) and some more, which simplify the code but are not supported by older versions of Visual Studio. ScottPlot developers typically work with the latest major version of [Visual Studio Community](https://visualstudio.microsoft.com/vs/community/), but it is possible to build ScottPlot using older tools with some modifications.

### Visual Studio 2019

> ⚠️ **WARNING: These steps are not required** for users who can use the latest free version of Visual Studio Community. Visual Studio 2019 is no longer the latest major version, but is officially supported until 2029 (see [Visual Studio 2019 Product Lifecycle](https://docs.microsoft.com/en-us/visualstudio/releases/2019/servicing-vs2019) for details).

> 🚀 **Special thanks to Lennox808** from the [ScottPlot Discord](/discord) for sharing these notes after successfully building ScottPlot 4 and ScottPlot 5 in Visual Studio 2019 (version 16.7.5) in September, 2022.

#### ScottPlot 4

Try to build the solution one project at a time, starting with `ScottPlot`. The following lists the build errors you will encounter and how to solve them. After fixing a build error, rebuild and repeat, until no errors remain.

* Edit the .csproj file to update target framework(s) to a single version known to be on your system (e.g., `net462`) as well as the language version (e.g., `9.0`).

* Revert file-scoped namespaces. Lines starting with `namespace` should be modified such that the `;` is replaced with a `{` and a `}` must be added to the bottom of the file. Repeatedly attempting to build the solution will identify files requiring this modification by the errors it produces.

* Remove the `$` sign or comment-out `Obsolete` lines that produce build errors.

* Resolve the lambda errors (in `Crosshair` and `Heatmap`) by replacing the expression with explicit logic.

Some of the projects of the ScottPlot solution require specific changes to make them compile with an older version of Visual Studio. The following lists the majority of these changes, nevertheless it may occure that this list may be incomplete.

* `Demos/Eto.Forms Demo`: Add *PresentationFramework* to Assemblys.

* `Demos/WinForms Demo`: Add *System.Windows.Forms* to Assemblys.

* `Demos/WPF Demo`: Replace the standard namespace from *WPF Demo* to *ScottPlot.Demo.WPF* in the project settings.

* `Sandbox/ConsoleApp`: Add `<LangVersion>9.0</LangVersion>` to the .csproj file.

* `Sandbox/WinFormsApp`: Add *System.Windows.Forms* to Assemblys.

* `Sandbox/WpfApp`: Open the .csproj file and change the first line from `<Project Sdk="Microsoft.NET.Sdk">` to `<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">`.

* `ScottPlot.Tests`: Add `<LangVersion>9.0</LangVersion>` to the .csproj file. Comment out the `using` statements that causes an error. Resolve the remaining errors.

#### ScottPlot 5

Even though *ScottPlot 5* is intended for people using .NET 7 and newer, it is still possible to build the solutions projects with older Visual Studio versions. Nevertheless this is only recommended for **debug** purposes as it requires quite some changes to the source files to make the projects compilable. For **contribution** purposes it is recommended to use Visual Studio 2022. 

Besides file-scoped namespaces, the `ScottPlot` project makes use of `global using` statements, which means that in every source file of this project the said statements have to be added, either manually or with an automated script. Furthermore the following steps are necessary.

* Replace the `readonly` properties in `Color.cs` with functions returning the color.

* Replace struct declarations with class declaration where needed. Don't use class declaration for `RenderDetails` and `PixelPadding`.

* Initialize properties with `null!` for non nullable properties with argument validation (error *CS8618*).

* Replace the error in `Heatmap` caused by a nullable property with an explicit check for null.

Some projects require specific changes in order to make them compilable with older versions of Visual Studio. The following list contains the typical changes required, but it may happen that this list is incomplete.

* `Controls/ScottPlot.WinForms`: Add *System.Windows.Forms* to Assemblys.

* `Controls/ScottPlot.WPF`: Open the .csproj file and change the first line from `<Project Sdk="Microsoft.NET.Sdk">` to `<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">`.

* `Demo/DemoLauncher`: Add `<LangVersion>9.0</LangVersion>` to the .csproj file. Revert file-scoped namespaces in `HtmlMenu.cs`.

* `Demo/WinForms Demo`: Add `<LangVersion>9.0</LangVersion>` to the .csproj file. Revert file-scoped namespaces in `CookbookViewer.cs`, `CookbookViewer.Designer.cs` and `DemoWindows.cs`. Add *System.Windows.Forms* to Assemblys. Add `using System.Windows.Forms`, `using System.Linq` and `using System` where necessary. Replace `ApplicationConfiguration.Initialize()`in `Program.cs` with `Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false);`. Open the project properties, go to "Build" and select "x64" as target platform. 

* `Sandbox/Sandbox.Avalonia`: Add `<LangVersion>9.0</LangVersion>` to the .csproj file.

* `Sandbox/Sandbox.Eto`: Add `using System` where necessary.

* `Sandbox/WPF`: Add `<LangVersion>9.0</LangVersion>` to the .csproj file and change the first line from `<Project Sdk="Microsoft.NET.Sdk">` to `<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">`.

* `ScottPlot Cookbook`: Add `<LangVersion>9.0</LangVersion>` to the .csproj file.

### Automated Conversion with Python

The processes above may be automated using [this python script](convert-vs2017.py) contributed by Lennox808 from the [ScottPlot Discord](/discord). It is not officially supported, but still seems quite useful.

### Troubleshooting

* On error code `NU1105` open the cmd, navigate to the respective folder containing the .csproj file and run the command `dotnet restore <project file>`.

* On error code `CS7036` "*There is no argument given that corresponds to the required formal parameter*" replace the class declaration with a struct declaration of the type of the object that causes the error.

* On runtime exception "*Unable to load DLL 'WebView2Loader.dll'*" make sure that the target-platform is x64. If you change the target-platform, do a rebuild.

* On error message "*SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true*", add `<DeterministicSourcePaths>false</DeterministicSourcePaths>` to the .csproj file inside a `<PropertyGroup>`.

And here is the Python converter script, it now takes the -ReplaceGlobalUsing, which enables the attachment of the using ... if a file was found that makes use of global using ... for that respective csproj project. I did some clean up and optimizations too to make the code more readable

"""
This script automates code changes required to build ScottPlot in VS2019.
Run this script without arguments for usage information.

Using the script:
The python script does the steps which else have to be done manually automatically. The script takes the following arguments: 
-Repository "PATH_TO_REPOSITORY": Folder from which for .csproj and .cs files are searched for and adapted
-TargetFramework "net472": Changes the <TargetFramework> content of each found .csproj file to net472 (net462 for example selects .NET Framework 4.6.2)
-LangVersion "9.0": Changes the <LangVersion> content of each found .csproj file to 9.0
-ReplaceNamespace: Enables the replacement of the namespace ScottPlot.x; with namespace ScottPlot.x {...}in all found .cs files
-ReplaceGlobalUsing: If a file is found that makes use of `global using ...;`, all .cs files of this project get the respective statement appended.
"""

import os
import sys
import re


## Class to hold and determine all arguments of this script
class Arguments:

    ## Creates an empty Arguments object.
    def __init__(self):
        self.PathRepository = "";
        self.TargetFramework = "";
        self.LangVersion = "";
        self.ReplaceNamespace = False;
        self.ReplaceGlobalUsing = False;
    
    ## Parses the given \p args which is a string list
    #  and sets the internal properties for further processing.
    def Parse(self, args):
        argc = len(args);
        for i in range(argc):
            currentArgument = args[i];
            nextArgument = "";
            
            if ((i + 1) < argc):
                nextArgument = args[i+1];

            if (sys.argv[i] == "-Repository"):
                self.PathRepository = nextArgument;
                i += 1;

            elif (sys.argv[i] == "-TargetFramework"):
                self.TargetFramework = nextArgument;
                i += 1;

            elif (sys.argv[i] == "-LangVersion"):
                self.LangVersion = nextArgument;
                i += 1;

            elif (sys.argv[i] == "-ReplaceNamespace"):
                self.ReplaceNamespace = True;

            elif (sys.argv[i] == "-ReplaceGlobalUsing"):
                self.ReplaceGlobalUsing = True;


## Searches for files with the given \p extension in the given
#   \p path and all its subdirectories and returns a list of
#  the absolute paths of these files.
def GetAllFiles(path, extension):
    foundFiles = [];
    if (os.path.isdir(path) == True):
        for path, subdirs, files in os.walk(path):
            for file in files:
                if (file.endswith("." + extension)):
                    foundFiles.append(os.path.join(path, file));
    return foundFiles;


## Same as GetAllFiles(), but searches for ".csproj" files
def GetAllProjectFiles(path):
    return GetAllFiles(path, "csproj");


## Same as GetAllFiles(), but searches for ".cs" files
def GetAllSourceFiles(path):
    return GetAllFiles(path, "cs");


## If the given \p content contains the XML Tag
#  "TargetFrameworks" or "TargetFramework", the content/value of this
#  tag is replaced with the new \p target. This function the returns
#  a string with the replaced \p content.
def ReplaceTargetFramework(content, target):
    content = re.sub('<TargetFrameworks>(.*)<\/TargetFrameworks>',
                     "<TargetFrameworks>" + target + "</TargetFrameworks>", content);
    content = re.sub('<TargetFramework>(.*)<\/TargetFramework>',
                     "<TargetFramework>" + target + "</TargetFramework>", content);
    return content;


## If the given \p content contains the XML Tag
#  "LangVersion" the content/value of this tag is replaced with the new \p version.
#  This function the returns a string with the replaced \p content.
def ReplaceLangVersion(content, version):
    content = re.sub('<LangVersion>(.*)<\/LangVersion>',
                     "<LangVersion>" + version + "</LangVersion>", content);
    return content;


## Reads the given \p file and returns its content as a single string.
def ReadFileContent(file):
    with open(file, 'r') as fileHandle:
        return fileHandle.read();


## (Over-)Write the given \p file with the given \p content.
def WriteFileContent(file, content):
    with open(file, 'w') as fileHandle:
        fileHandle.write(content);


## Returns True if the given \p content makes use of "global using ..." statements.
def ContainsGlobalUsing(content):
    return bool(re.search('global using (.*);[\r]{0,1}\n', content));


## Returns a list/array of all the "global using ..." statements used in the
#  given \p content.
def GetGlobalUsingStatements(content):
    pattern = "global using (.*);[\r]{0,1}\n";
    return re.findall(pattern, content);


## Returns the path to the .csproj file of the given \p sourceFile. To do so it is
#  assumed that the source files of a .csproj file are in the directory of the said
#  project file or in its containing (sub-)folders. That means, as soon as a folder
#  is reached, which contains a ".gitignore" file, which indicates the root of the
#  repository, this function stops.
def GetSourceFilesProjectFile(sourceFile):
    projectFile = "";
    if (os.path.exists(sourceFile)):
        currentDirectory = os.path.dirname(sourceFile);

        # Go down the directories until we reach the repositories root directory
        # A check for ".gitignore" is better than to check for ".git" as the latter
        # is not always there when the repository is downloaded instead of cloned.
        while ((".gitignore" in os.listdir(currentDirectory)) == False):
            # Get all files in the current directory only ...
            for filename in os.listdir(currentDirectory):
                filepath = os.path.join(currentDirectory, filename);

                # ... and check if current path is a csproj file
                if (os.path.isfile(filepath) and filepath.endswith(".csproj")):
                    projectFile = filepath;
                    break;
            
            # No csproj file found, go down one directory
            currentDirectory = os.path.join(currentDirectory, "..");

    return projectFile;


def main(args):

    arguments = Arguments();
    arguments.Parse(args);

    if (arguments.PathRepository == ""):
        print("Please set a -Repository")
        return 1;
    
    if (os.path.isdir(arguments.PathRepository) == False):
        print ("The given -Repository \"" + arguments.PathRepository + "\" does not exist");
        return 1;

    print("Input:");
    print("- Repository: \"" + arguments.PathRepository + "\"")
    print("- TargetFramework: \"" + arguments.TargetFramework + "\"")
    print("- LangVersion: \"" + arguments.LangVersion + "\"")
    print("- ReplaceNamespace: " + str(arguments.ReplaceNamespace));
    print("- ReplaceGlobalUsing: " + str(arguments.ReplaceGlobalUsing));

    projectFiles = GetAllProjectFiles(arguments.PathRepository)

    # Replacing the .csproj files "TargetFramework" and "LangVersion"
    # if necessary
    editCsprojFile = (arguments.TargetFramework != "") or (arguments.LangVersion != "");
    if (editCsprojFile):
        for projectFile in projectFiles:
            # Read in the file
            filecontent = ReadFileContent(projectFile);

            # Change the TargetFramework if wanted
            if (arguments.TargetFramework != ""):
                filecontent = ReplaceTargetFramework(filecontent, arguments.TargetFramework)

            # Change the LangVersion if wanted
            if (arguments.LangVersion != ""):
                filecontent = ReplaceLangVersion(filecontent, arguments.LangVersion)

            # Write the file out again
            WriteFileContent(projectFile, filecontent);

    # Replace "namespace ScottPlot.x;" with "namespace ScottPlot.x {...}"
    # if necessary
    if (arguments.ReplaceNamespace == True):
        # Yes, so search for all source files
        sourceFiles = GetAllSourceFiles(arguments.PathRepository)
        # Todo: Maybe not just "namespace ScottPlot.xxx;" ?
        regExpression = re.compile(r'namespace ScottPlot(.*);\n')

        # Go through all found source files
        for sourceFile in sourceFiles:
            # Expect the current file not to be changed
            writeFile = False
            filecontent = ReadFileContent(sourceFile);

            # Does this file contain the said "namespace ScottPlot.x;" pattern?
            if regExpression.search(filecontent):
                # Yes it does, replace the temporary data with "namespace ScottPlot.x {...}"...
                filecontent = re.sub(
                    r'namespace ScottPlot(.*);\n', r"namespace ScottPlot\g<1>\n{\n", filecontent)
                filecontent = filecontent + "\n}"
                # ... and mark the file to be written again
                writeFile = True

            # Only write the file if there was a change
            if (writeFile == True):
                WriteFileContent(sourceFile, filecontent);
    
    # Search for "global using ..." statements and replace all source files with the found statements?
    if (arguments.ReplaceGlobalUsing == True):
        # Yes, get all source files ...
        sourceFiles = GetAllSourceFiles(arguments.PathRepository);
        # ... and prepare a list that stores the paths to the files
        # that makes use of "global using ..."
        globalUsingSourceFiles = [];

        # Go through all source files and check if any makes use of "global using ..." statements
        for sourceFile in sourceFiles:
            filecontent = ReadFileContent(sourceFile);

            if (ContainsGlobalUsing(filecontent) == True):
                # Source file makes use of "global using ..."
                globalUsingSourceFiles.append(sourceFile);
        
        # Now find out to which project the files belong which makes use of "global using ..."
        for globalUsingSourceFile in globalUsingSourceFiles:
            # To do so it is assumed, that the csproj file is the root folder of all the
            # source files of the project, in which the "using ..." statements have to be
            # appended
            sourceFilesProjectFile = GetSourceFilesProjectFile(globalUsingSourceFile);
            if (sourceFilesProjectFile != ""):
                # Find out which "global using ..." statements are used in this project ...
                projectsGlobalUsingStatements = GetGlobalUsingStatements(ReadFileContent(globalUsingSourceFile));

                # ... then go through all source files of this project ...
                projectSourceFiles = GetAllSourceFiles(os.path.dirname(sourceFilesProjectFile));
                for projectSourceFile in projectSourceFiles:
                    filecontent = ReadFileContent(projectSourceFile);
                    writeFile = False;
                    # ... check if the any source file already
                    # contains the "global using ..." statement(s)
                    for projectsGlobalUsingStatement in projectsGlobalUsingStatements:
                        currentUsingStatement = "using " + projectsGlobalUsingStatement + ";";
                        # ... and if not, add only the missing statements
                        # at the beginning of the source file
                        if currentUsingStatement not in filecontent:
                            filecontent = currentUsingStatement + "\n" + filecontent;
                            writeFile = True;
                    
                    # Only write the current source file if a using statement was added
                    if (writeFile == True):
                        WriteFileContent(projectSourceFile, filecontent);


if __name__ == "__main__":
    noArgumentsGiven = len(sys.argv) == 1;
    if (noArgumentsGiven):
        print("""Usage:

    -Repository \"PATH\" (mandatory)
    Path to the ScottPlot repository. Absolute recommended.

    -TargetFramework \"TARGET\" (mandatory)
    Replaces the \"TargetFramework\" and \"TargetFrameworks\" tags
    with the given TARGET. For example \"net472\".

    -LangVersion \"VERSION\" (optional, but recommended)
    Replaces the \"LangVersion\" tags
    with the given VERSION. For example \"9.0\", in order to
    use \"Target-typed New Expressions\"

    -ReplaceNamespace (optional)
    Replaces the ScottPlot file scoped namespaces with curly bracets. For example
    \"namespace ScottPlot.Plottable;\" becomes \"namespace ScottPlot.Plottable {...}\"

    -ReplaceGlobalUsing: If a file is found that makes use of `global using ...;`,
    all .cs files of this project get the respective statement appended.
    """)
    else:
        main(sys.argv)
@swharden
Copy link
Member Author

This issue is linked to on https://scottplot.net/faq/environment/#scottplot-5 and can be extended with new information as it is contributed by users

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant