From 9628c70f7783023b917434f09cdcacedb8be5a85 Mon Sep 17 00:00:00 2001 From: Gary Oberbrunner Date: Thu, 22 Jul 2010 00:50:52 +0000 Subject: Integrated patch for issue 2571, various fixes for MSVS project generation. From Jean-Franois Colson. --- QMTest/TestSConsMSVS.py | 49 ++++++------ src/CHANGES.txt | 11 ++- src/engine/SCons/Tool/msvs.py | 165 ++++++++++++++++++++++++++++------------ src/engine/SCons/Tool/msvs.xml | 67 ++++++++-------- test/MSVS/common-prefix.py | 2 - test/MSVS/runfile.py | 2 - test/MSVS/vs-7.0-variant_dir.py | 7 +- test/MSVS/vs-7.1-variant_dir.py | 7 +- test/MSVS/vs-8.0-variant_dir.py | 11 ++- 9 files changed, 202 insertions(+), 119 deletions(-) diff --git a/QMTest/TestSConsMSVS.py b/QMTest/TestSConsMSVS.py index ffba6d2..4ed9b06 100644 --- a/QMTest/TestSConsMSVS.py +++ b/QMTest/TestSConsMSVS.py @@ -188,17 +188,18 @@ env.MSVSProject(target = 'Test.dsp', expected_slnfile_7_0 = """\ Microsoft Visual Studio Solution File, Format Version 7.00 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Test", "Test.vcproj", "{E5466E26-0003-F18B-8F8A-BCD76C86388D}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Test", "Test.vcproj", "" EndProject Global + \tGlobalSection(SolutionConfiguration) = preSolution \t\tConfigName.0 = Release \tEndGlobalSection \tGlobalSection(ProjectDependencies) = postSolution \tEndGlobalSection \tGlobalSection(ProjectConfiguration) = postSolution -\t\t{E5466E26-0003-F18B-8F8A-BCD76C86388D}.Release.ActiveCfg = Release|Win32 -\t\t{E5466E26-0003-F18B-8F8A-BCD76C86388D}.Release.Build.0 = Release|Win32 +\t\t.Release.ActiveCfg = Release|Win32 +\t\t.Release.Build.0 = Release|Win32 \tEndGlobalSection \tGlobalSection(ExtensibilityGlobals) = postSolution \tEndGlobalSection @@ -208,14 +209,13 @@ EndGlobal """ expected_vcprojfile_7_0 = """\ - + \tKeyword="MakeFileProj"> \t \t\t \t\t \t @@ -310,17 +310,20 @@ env.MSVSProject(target = 'Test.vcproj', expected_slnfile_7_1 = """\ Microsoft Visual Studio Solution File, Format Version 8.00 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Test", "Test.vcproj", "{E5466E26-0003-F18B-8F8A-BCD76C86388D}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Test", "Test.vcproj", "" \tProjectSection(ProjectDependencies) = postProject \tEndProjectSection EndProject Global + \tGlobalSection(SolutionConfiguration) = preSolution \t\tConfigName.0 = Release \tEndGlobalSection +\tGlobalSection(ProjectDependencies) = postSolution +\tEndGlobalSection \tGlobalSection(ProjectConfiguration) = postSolution -\t\t{E5466E26-0003-F18B-8F8A-BCD76C86388D}.Release.ActiveCfg = Release|Win32 -\t\t{E5466E26-0003-F18B-8F8A-BCD76C86388D}.Release.Build.0 = Release|Win32 +\t\t.Release.ActiveCfg = Release|Win32 +\t\t.Release.Build.0 = Release|Win32 \tEndGlobalSection \tGlobalSection(ExtensibilityGlobals) = postSolution \tEndGlobalSection @@ -330,14 +333,13 @@ EndGlobal """ expected_vcprojfile_7_1 = """\ - + \tKeyword="MakeFileProj"> \t \t\t \t\t \t @@ -435,15 +437,16 @@ env.MSVSProject(target = 'Test.vcproj', expected_slnfile_8_0 = """\ Microsoft Visual Studio Solution File, Format Version 9.00 # Visual Studio 2005 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Test", "Test.vcproj", "{E5466E26-0003-F18B-8F8A-BCD76C86388D}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Test", "Test.vcproj", "" EndProject Global + \tGlobalSection(SolutionConfigurationPlatforms) = preSolution \t\tRelease|Win32 = Release|Win32 \tEndGlobalSection \tGlobalSection(ProjectConfigurationPlatforms) = postSolution -\t\t{E5466E26-0003-F18B-8F8A-BCD76C86388D}.Release|Win32.ActiveCfg = Release|Win32 -\t\t{E5466E26-0003-F18B-8F8A-BCD76C86388D}.Release|Win32.Build.0 = Release|Win32 +\t\t.Release|Win32.ActiveCfg = Release|Win32 +\t\t.Release|Win32.Build.0 = Release|Win32 \tEndGlobalSection \tGlobalSection(SolutionProperties) = preSolution \t\tHideSolutionNode = FALSE @@ -458,9 +461,8 @@ expected_vcprojfile_8_0 = """\ \tVersion="8.00" \tName="Test" \tProjectGUID="" -\tSccProjectName="" -\tSccLocalPath="" \tRootNamespace="Test" + \tKeyword="MakeFileProj"> \t \t\t + """ @@ -536,8 +573,8 @@ V7DSPConfiguration = """\ \t\t\t \t\t """ @@ -548,8 +585,9 @@ V8DSPHeader = """\ \tProjectType="Visual C++" \tVersion="%(versionstr)s" \tName="%(name)s" -%(scc_attrs)s +\tProjectGUID="%(project_guid)s" \tRootNamespace="%(name)s" +%(scc_attrs)s \tKeyword="MakeFileProj"> """ @@ -603,20 +641,27 @@ class _GenerateV7DSP(_DSPGenerator): scc_provider = env.get('MSVS_SCC_PROVIDER', '') scc_project_name = env.get('MSVS_SCC_PROJECT_NAME', '') scc_aux_path = env.get('MSVS_SCC_AUX_PATH', '') - scc_local_path = env.get('MSVS_SCC_LOCAL_PATH', '') + # MSVS_SCC_LOCAL_PATH is kept for backwards compatibility purpose and should + # be deprecated as soon as possible. + scc_local_path_legacy = env.get('MSVS_SCC_LOCAL_PATH', '') + scc_connection_root = env.get('MSVS_SCC_CONNECTION_ROOT', os.curdir) + scc_local_path = os.path.relpath(scc_connection_root, os.path.dirname(self.dspabs)) project_guid = env.get('MSVS_PROJECT_GUID', '') - if self.version_num >= 8.0 and not project_guid: + if not project_guid: project_guid = _generateGUID(self.dspfile, '') if scc_provider != '': - scc_attrs = ('\tProjectGUID="%s"\n' - '\tSccProjectName="%s"\n' - '\tSccAuxPath="%s"\n' - '\tSccLocalPath="%s"\n' - '\tSccProvider="%s"' % (project_guid, scc_project_name, scc_aux_path, scc_local_path, scc_provider)) + scc_attrs = '\tSccProjectName="%s"\n' % scc_project_name + if scc_aux_path != '': + scc_attrs += '\tSccAuxPath="%s"\n' % scc_aux_path + scc_attrs += ('\tSccLocalPath="%s"\n' + '\tSccProvider="%s"' % (scc_local_path, scc_provider)) + elif scc_local_path_legacy != '': + # This case is kept for backwards compatibility purpose and should + # be deprecated as soon as possible. + scc_attrs = ('\tSccProjectName="%s"\n' + '\tSccLocalPath="%s"' % (scc_project_name, scc_local_path_legacy)) else: - scc_attrs = ('\tProjectGUID="%s"\n' - '\tSccProjectName="%s"\n' - '\tSccLocalPath="%s"' % (project_guid, scc_project_name, scc_local_path)) + self.dspheader = self.dspheader.replace('%(scc_attrs)s\n', '') self.file.write(self.dspheader % locals()) @@ -816,6 +861,7 @@ class _DSWGenerator(object): """ Base class for DSW generators """ def __init__(self, dswfile, source, env): self.dswfile = os.path.normpath(str(dswfile)) + self.dsw_folder_path = os.path.dirname(os.path.abspath(self.dswfile)) self.env = env if 'projects' not in env: @@ -896,6 +942,26 @@ class _GenerateV7DSW(_DSWGenerator): if not platform in self.platforms: self.platforms.append(platform) + def GenerateProjectFilesInfo(self): + for dspfile in self.dspfiles: + dsp_folder_path, name = os.path.split(dspfile) + dsp_folder_path = os.path.abspath(dsp_folder_path) + dsp_relative_folder_path = os.path.relpath(dsp_folder_path, self.dsw_folder_path) + if dsp_relative_folder_path == os.curdir: + dsp_relative_file_path = name + else: + dsp_relative_file_path = os.path.join(dsp_relative_folder_path, name) + dspfile_info = {'NAME': name, + 'GUID': _generateGUID(dspfile, ''), + 'FOLDER_PATH': dsp_folder_path, + 'FILE_PATH': dspfile, + 'SLN_RELATIVE_FOLDER_PATH': dsp_relative_folder_path, + 'SLN_RELATIVE_FILE_PATH': dsp_relative_file_path} + self.dspfiles_info.append(dspfile_info) + + self.dspfiles_info = [] + GenerateProjectFilesInfo(self) + def Parse(self): try: dswfile = open(self.dswfile,'r') @@ -930,14 +996,13 @@ class _GenerateV7DSW(_DSWGenerator): self.file.write('Microsoft Visual Studio Solution File, Format Version %s\n' % self.versionstr ) if self.version_num >= 8.0: self.file.write('# Visual Studio 2005\n') - for p in self.dspfiles: - name = os.path.basename(p) + for dspinfo in self.dspfiles_info: + name = dspinfo['NAME'] base, suffix = SCons.Util.splitext(name) if suffix == '.vcproj': name = base - guid = _generateGUID(p, '') self.file.write('Project("%s") = "%s", "%s", "%s"\n' - % ( external_makefile_guid, name, p, guid ) ) + % (external_makefile_guid, name, dspinfo['SLN_RELATIVE_FILE_PATH'], dspinfo['GUID'])) if self.version_num >= 7.1 and self.version_num < 8.0: self.file.write('\tProjectSection(ProjectDependencies) = postProject\n' '\tEndProjectSection\n') @@ -947,30 +1012,36 @@ class _GenerateV7DSW(_DSWGenerator): env = self.env if 'MSVS_SCC_PROVIDER' in env: - dspfile_base = os.path.basename(self.dspfile) + scc_number_of_projects = len(self.dspfiles) + 1 slnguid = self.slnguid - scc_provider = env.get('MSVS_SCC_PROVIDER', '') - scc_provider = scc_provider.replace(' ', r'\u0020') - scc_project_name = env.get('MSVS_SCC_PROJECT_NAME', '') - # scc_aux_path = env.get('MSVS_SCC_AUX_PATH', '') - scc_local_path = env.get('MSVS_SCC_LOCAL_PATH', '') - scc_project_base_path = env.get('MSVS_SCC_PROJECT_BASE_PATH', '') - # project_guid = env.get('MSVS_PROJECT_GUID', '') - + scc_provider = env.get('MSVS_SCC_PROVIDER', '').replace(' ', r'\u0020') + scc_project_name = env.get('MSVS_SCC_PROJECT_NAME', '').replace(' ', r'\u0020') + scc_connection_root = env.get('MSVS_SCC_CONNECTION_ROOT', os.curdir) + scc_local_path = os.path.relpath(scc_connection_root, self.dsw_folder_path).replace('\\', '\\\\') self.file.write('\tGlobalSection(SourceCodeControl) = preSolution\n' - '\t\tSccNumberOfProjects = 2\n' - '\t\tSccProjectUniqueName0 = %(dspfile_base)s\n' + '\t\tSccNumberOfProjects = %(scc_number_of_projects)d\n' + '\t\tSccProjectName0 = %(scc_project_name)s\n' '\t\tSccLocalPath0 = %(scc_local_path)s\n' - '\t\tCanCheckoutShared = true\n' - '\t\tSccProjectFilePathRelativizedFromConnection0 = %(scc_project_base_path)s\n' - '\t\tSccProjectName1 = %(scc_project_name)s\n' - '\t\tSccLocalPath1 = %(scc_local_path)s\n' - '\t\tSccProvider1 = %(scc_provider)s\n' - '\t\tCanCheckoutShared = true\n' - '\t\tSccProjectFilePathRelativizedFromConnection1 = %(scc_project_base_path)s\n' - '\t\tSolutionUniqueID = %(slnguid)s\n' - '\tEndGlobalSection\n' % locals()) - + '\t\tSccProvider0 = %(scc_provider)s\n' + '\t\tCanCheckoutShared = true\n' % locals()) + sln_relative_path_from_scc = os.path.relpath(self.dsw_folder_path, scc_connection_root) + if sln_relative_path_from_scc != os.curdir: + self.file.write('\t\tSccProjectFilePathRelativizedFromConnection0 = %s\\\\\n' + % sln_relative_path_from_scc.replace('\\', '\\\\')) + if self.version_num < 8.0: + # When present, SolutionUniqueID is automatically removed by VS 2005 + # TODO: check for Visual Studio versions newer than 2005 + self.file.write('\t\tSolutionUniqueID = %s\n' % slnguid) + for dspinfo in self.dspfiles_info: + i = self.dspfiles_info.index(dspinfo) + 1 + dsp_relative_file_path = dspinfo['SLN_RELATIVE_FILE_PATH'].replace('\\', '\\\\') + dsp_scc_relative_folder_path = os.path.relpath(dspinfo['FOLDER_PATH'], scc_connection_root).replace('\\', '\\\\') + self.file.write('\t\tSccProjectUniqueName%(i)s = %(dsp_relative_file_path)s\n' + '\t\tSccLocalPath%(i)d = %(scc_local_path)s\n' + '\t\tCanCheckoutShared = true\n' + '\t\tSccProjectFilePathRelativizedFromConnection%(i)s = %(dsp_scc_relative_folder_path)s\\\\\n' + % locals()) + self.file.write('\tEndGlobalSection\n') if self.version_num >= 8.0: self.file.write('\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n') else: @@ -987,7 +1058,7 @@ class _GenerateV7DSW(_DSWGenerator): self.file.write('\t\tConfigName.%d = %s\n' % (cnt, variant)) cnt = cnt + 1 self.file.write('\tEndGlobalSection\n') - if self.version_num < 7.1: + if self.version_num <= 7.1: self.file.write('\tGlobalSection(ProjectDependencies) = postSolution\n' '\tEndGlobalSection\n') if self.version_num >= 8.0: @@ -999,13 +1070,13 @@ class _GenerateV7DSW(_DSWGenerator): variant = self.configs[name].variant platform = self.configs[name].platform if self.version_num >= 8.0: - for p in self.dspfiles: - guid = _generateGUID(p, '') + for dspinfo in self.dspfiles_info: + guid = dspinfo['GUID'] self.file.write('\t\t%s.%s|%s.ActiveCfg = %s|%s\n' '\t\t%s.%s|%s.Build.0 = %s|%s\n' % (guid,variant,platform,variant,platform,guid,variant,platform,variant,platform)) else: - for p in self.dspfiles: - guid = _generateGUID(p, '') + for dspinfo in self.dspfiles_info: + guid = dspinfo['GUID'] self.file.write('\t\t%s.%s.ActiveCfg = %s|%s\n' '\t\t%s.%s.Build.0 = %s|%s\n' %(guid,variant,variant,platform,guid,variant,variant,platform)) @@ -1072,7 +1143,7 @@ class _GenerateV6DSW(_DSWGenerator): def PrintWorkspace(self): """ writes a DSW file """ name = self.name - dspfile = self.dspfiles[0] + dspfile = os.path.relpath(self.dspfiles[0], self.dsw_folder_path) self.file.write(V6DSWHeader % locals()) def Build(self): @@ -1118,10 +1189,10 @@ def GenerateDSW(dswfile, source, env): ############################################################################## def GetMSVSProjectSuffix(target, source, env, for_signature): - return env['MSVS']['PROJECTSUFFIX'] + return env['MSVS']['PROJECTSUFFIX'] def GetMSVSSolutionSuffix(target, source, env, for_signature): - return env['MSVS']['SOLUTIONSUFFIX'] + return env['MSVS']['SOLUTIONSUFFIX'] def GenerateProject(target, source, env): # generate the dsp file, according to the version of MSVS. @@ -1246,7 +1317,7 @@ def projectEmitter(target, source, env): sourcelist = source if env.get('auto_build_solution', 1): - env['projects'] = targetlist + env['projects'] = [env.File(t).srcnode() for t in targetlist] t, s = solutionEmitter(target, target, env) targetlist = targetlist + t diff --git a/src/engine/SCons/Tool/msvs.xml b/src/engine/SCons/Tool/msvs.xml index 9d5e2da..9ac20b0 100644 --- a/src/engine/SCons/Tool/msvs.xml +++ b/src/engine/SCons/Tool/msvs.xml @@ -287,21 +287,6 @@ will generate an error. - - -The string -placed in a generated Microsoft Visual Studio solution file -as the value of the -SccProjectFilePathRelativizedFromConnection0 -and -SccProjectFilePathRelativizedFromConnection1 -attributes of the -GlobalSection(SourceCodeControl) -section. -There is no default value. - - - The string @@ -309,12 +294,7 @@ placed in a generated Microsoft Visual Studio project file as the value of the ProjectGUID attribute. -The string is also placed in the -SolutionUniqueID -attribute of the -GlobalSection(SourceCodeControl) -section of the Microsoft Visual Studio solution file. -There is no default value. +There is no default value. If not defined, a new GUID is generated. @@ -332,24 +312,33 @@ There is no default value. - + -The path name -placed in a generated Microsoft Visual Studio project file -as the value of the +The root path of projects in your SCC workspace, i.e the path under which +all project and solution files will be generated. It is used as a +reference path from which the relative paths of the generated +Microsoft Visual Studio project and solution files are computed. +The relative project file path is placed as the value of the SccLocalPath attribute -if the -MSVS_SCC_PROVIDER -construction variable is also set. -The path name is also placed in the -SccLocalPath0 -and -SccLocalPath1 +of the project file +and as the values of the +SccProjectFilePathRelativizedFromConnection[i] +(where [i] ranges from 0 to the number of projects in the solution) attributes of the GlobalSection(SourceCodeControl) section of the Microsoft Visual Studio solution file. -There is no default value. +Similarly the relative solution file path is placed as the values of the +SccLocalPath[i] +(where [i] ranges from 0 to the number of projects in the solution) +attributes of the +GlobalSection(SourceCodeControl) +section of the Microsoft Visual Studio solution file. +This is used only +if the +MSVS_SCC_PROVIDER +construction variable is also set. +The default value is the current working directory. @@ -359,7 +348,15 @@ The project name placed in a generated Microsoft Visual Studio project file as the value of the SccProjectName -attribute. +attribute +if the +MSVS_SCC_PROVIDER +construction variable is also set. +In this case the string is also placed in the +SccProjectName0 +attribute of the +GlobalSection(SourceCodeControl) +section of the Microsoft Visual Studio solution file. There is no default value. @@ -372,7 +369,7 @@ as the value of the SccProvider attribute. The string is also placed in the -SccProvider1 +SccProvider0 attribute of the GlobalSection(SourceCodeControl) section of the Microsoft Visual Studio solution file. diff --git a/test/MSVS/common-prefix.py b/test/MSVS/common-prefix.py index debe992..7079661 100644 --- a/test/MSVS/common-prefix.py +++ b/test/MSVS/common-prefix.py @@ -48,8 +48,6 @@ vcproj_template = """\ \tVersion="8.00" \tName="Test" \tProjectGUID="" -\tSccProjectName="" -\tSccLocalPath="" \tRootNamespace="Test" \tKeyword="MakeFileProj"> \t diff --git a/test/MSVS/runfile.py b/test/MSVS/runfile.py index 7c85a16..631c919 100644 --- a/test/MSVS/runfile.py +++ b/test/MSVS/runfile.py @@ -48,8 +48,6 @@ expected_vcprojfile = """\ \tVersion="8.00" \tName="Test" \tProjectGUID="" -\tSccProjectName="" -\tSccLocalPath="" \tRootNamespace="Test" \tKeyword="MakeFileProj"> \t diff --git a/test/MSVS/vs-7.0-variant_dir.py b/test/MSVS/vs-7.0-variant_dir.py index 9194785..5699663 100644 --- a/test/MSVS/vs-7.0-variant_dir.py +++ b/test/MSVS/vs-7.0-variant_dir.py @@ -56,14 +56,17 @@ test.write(['src', 'SConscript'], SConscript_contents%{'HOST_ARCH': host_arch}) test.run(arguments=".") +project_guid = "{25F6CE89-8E22-2910-8B6E-FFE6DC1E2792}" vcproj = test.read(['src', 'Test.vcproj'], 'r') -expect = test.msvs_substitute(expected_vcprojfile, '7.0', None, 'SConstruct') +expect = test.msvs_substitute(expected_vcprojfile, '7.0', None, 'SConstruct', + project_guid=project_guid) # don't compare the pickled data assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) test.must_exist(test.workpath('src', 'Test.sln')) sln = test.read(['src', 'Test.sln'], 'r') -expect = test.msvs_substitute(expected_slnfile, '7.0', 'src') +expect = test.msvs_substitute(expected_slnfile, '7.0', 'src', + project_guid=project_guid) # don't compare the pickled data assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) diff --git a/test/MSVS/vs-7.1-variant_dir.py b/test/MSVS/vs-7.1-variant_dir.py index 82b0be3..3a25651 100644 --- a/test/MSVS/vs-7.1-variant_dir.py +++ b/test/MSVS/vs-7.1-variant_dir.py @@ -56,14 +56,17 @@ test.write(['src', 'SConscript'], SConscript_contents%{'HOST_ARCH': host_arch}) test.run(arguments=".") +project_guid = "{25F6CE89-8E22-2910-8B6E-FFE6DC1E2792}" vcproj = test.read(['src', 'Test.vcproj'], 'r') -expect = test.msvs_substitute(expected_vcprojfile, '7.0', None, 'SConstruct') +expect = test.msvs_substitute(expected_vcprojfile, '7.0', None, 'SConstruct', + project_guid=project_guid) # don't compare the pickled data assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) test.must_exist(test.workpath('src', 'Test.sln')) sln = test.read(['src', 'Test.sln'], 'r') -expect = test.msvs_substitute(expected_slnfile, '7.0', 'src') +expect = test.msvs_substitute(expected_slnfile, '7.0', 'src', + project_guid=project_guid) # don't compare the pickled data assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) diff --git a/test/MSVS/vs-8.0-variant_dir.py b/test/MSVS/vs-8.0-variant_dir.py index 9f9098a..20c7381 100644 --- a/test/MSVS/vs-8.0-variant_dir.py +++ b/test/MSVS/vs-8.0-variant_dir.py @@ -56,18 +56,17 @@ test.write(['src', 'SConscript'], SConscript_contents%{'HOST_ARCH': host_arch}) test.run(arguments=".") +project_guid = "{25F6CE89-8E22-2910-8B6E-FFE6DC1E2792}" vcproj = test.read(['src', 'Test.vcproj'], 'r') -expect = test.msvs_substitute(expected_vcprojfile, - '8.0', - None, - 'SConstruct', - project_guid="{25F6CE89-8E22-2910-8B6E-FFE6DC1E2792}") +expect = test.msvs_substitute(expected_vcprojfile, '8.0', None, 'SConstruct', + project_guid=project_guid) # don't compare the pickled data assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) test.must_exist(test.workpath('src', 'Test.sln')) sln = test.read(['src', 'Test.sln'], 'r') -expect = test.msvs_substitute(expected_slnfile, '8.0', 'src') +expect = test.msvs_substitute(expected_slnfile, '8.0', 'src', + project_guid=project_guid) # don't compare the pickled data assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) -- cgit v0.12