diff --git a/cli/package-lock.json b/cli/package-lock.json index 7ae69c5..9b248c6 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ibm/sourceorbit", - "version": "1.0.2", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ibm/sourceorbit", - "version": "1.0.2", + "version": "1.1.0", "license": "Apache 2", "dependencies": { "crc-32": "https://cdn.sheetjs.com/crc-32-latest/crc-32-latest.tgz" diff --git a/cli/src/builders/actions/index.ts b/cli/src/builders/actions/index.ts new file mode 100644 index 0000000..695e47b --- /dev/null +++ b/cli/src/builders/actions/index.ts @@ -0,0 +1,85 @@ +import { ReadFileSystem } from "../../readFileSystem"; +import { Targets } from "../../targets"; +import path from "path"; + +export interface Action { + name: string, + command: string, + environment: "ile", + extensions: string[], +} + +export class ProjectActions { + private actions: { [relativePath: string]: Action[] } = {}; + + constructor(private readonly targets: Targets, private readonly readFileSystem: ReadFileSystem) {} + + get getActionPaths() { + return Object.keys(this.actions); + } + + public async loadAllActions() { + const cwd = this.targets.getCwd(); + const files = await this.readFileSystem.getFiles(cwd, `**/actions.json`, {dot: true}); + + for (const file of files) { + const relativePath = path.relative(cwd, file); + const contents = await this.readFileSystem.readFile(file); + try { + const possibleActions = JSON.parse(contents) as Partial; + + for (const possibleAction of possibleActions) { + if (!possibleAction.name || !possibleAction.command || !possibleAction.environment || !possibleAction.extensions) { + // TODO: Log a warning about missing required fields + continue; // Skip if required fields are missing + } + + possibleAction.extensions = possibleAction.extensions.map(ext => ext.toLowerCase()); + } + + this.actions[relativePath] = possibleActions as Action[]; + } catch (e) { + console.log(`Error parsing actions.json at ${relativePath}:`, e); + } + } + + const vscodePath = path.join(`.vscode`, `actions.json`); + if (this.actions[vscodePath]) { + // If there is a .vscode/actions.json, it is the project actions + this.actions[`actions.json`] = this.actions[vscodePath]; + delete this.actions[vscodePath]; + } + } + + getActionForPath(relativeSourcePath: string): Action|undefined { + let allPossibleActions: Action[] = []; + let parent: string = relativeSourcePath; + + while (true) { + parent = path.dirname(parent); + + const actionFile = path.join(parent, `actions.json`); + + if (this.actions[actionFile]) { + allPossibleActions.push(...this.actions[actionFile]); + } + + if (parent === `.`) { + // Reached the root directory, stop searching + break; + } + } + + let extension = path.extname(relativeSourcePath).toLowerCase(); + + if (extension.startsWith(`.`)) { + extension = extension.slice(1); + } + + allPossibleActions = allPossibleActions.filter((action) => { + return action.extensions.includes(extension); + }); + + return allPossibleActions[0]; + } +} \ No newline at end of file diff --git a/cli/src/builders/environment.ts b/cli/src/builders/environment.ts index fb20926..27a92f5 100644 --- a/cli/src/builders/environment.ts +++ b/cli/src/builders/environment.ts @@ -1,5 +1,6 @@ import { str } from "crc-32/crc32c"; import { ObjectType } from "../targets"; +import path from "path"; // Always try and store parmId as lowercase export type CommandParameters = { [parmId: string]: string }; @@ -43,6 +44,22 @@ export function extCanBeProgram(ext: string): boolean { return ([`MODULE`, `PGM`].includes(getObjectType(ext))); } +export function getTrueBasename(name: string) { + // Logic to handle second extension, caused by bob. + const sourceObjectTypes = [`.PGM`, `.SRVPGM`, `.TEST`]; + const secondName = path.parse(name); + if (secondName.ext && sourceObjectTypes.includes(secondName.ext.toUpperCase())) { + name = secondName.name; + } + + // Remove bob text convention + if (name.includes(`-`)) { + name = name.substring(0, name.indexOf(`-`)); + } + + return name; +} + export function getObjectType(ext: string): ObjectType { switch (ext.toLowerCase()) { case `dspf`: diff --git a/cli/src/builders/iProject.ts b/cli/src/builders/iProject.ts index c48c930..d17d547 100644 --- a/cli/src/builders/iProject.ts +++ b/cli/src/builders/iProject.ts @@ -1,5 +1,4 @@ -import { fromCl } from "../utils"; -import { CompileData, CommandParameters, CompileAttribute, getDefaultCompiles, Action, getObjectType } from "./environment"; +import { CommandParameters, CompileAttribute, getDefaultCompiles } from "./environment"; export class iProject { includePaths?: string[] = []; @@ -33,18 +32,4 @@ export class iProject { } } } - - applyAction(newAction: Action) { - if (newAction.environment && newAction.environment === `ile` && newAction.extensions && newAction.extensions.length > 0) { - if (!newAction.extensions.includes(`GLOBAL`)) { - const firstExt = newAction.extensions[0].toLowerCase(); - const becomesObject = getObjectType(firstExt); - const commandData = fromCl(newAction.command); - this.compiles[firstExt] = { - becomes: becomesObject, - ...commandData - }; - } - } - } } \ No newline at end of file diff --git a/cli/src/builders/make/customRules.ts b/cli/src/builders/make/customRules.ts index e960601..23e2133 100644 --- a/cli/src/builders/make/customRules.ts +++ b/cli/src/builders/make/customRules.ts @@ -6,7 +6,7 @@ import * as path from "path"; import { MakeProject } from "."; /** - * Scan for all rules.mk files and read attributes and custom + * Scan for all ibmi-bob rules.mk files and read attributes and custom * dependencies into the targets. */ export function readAllRules(targets: Targets, project: MakeProject) { diff --git a/cli/src/builders/make/index.ts b/cli/src/builders/make/index.ts index d6b7f1c..fcb1d82 100644 --- a/cli/src/builders/make/index.ts +++ b/cli/src/builders/make/index.ts @@ -1,31 +1,37 @@ import { existsSync, readFileSync } from 'fs'; import path from 'path'; import { ILEObject, ILEObjectTarget, ImpactedObject, ObjectType, Targets } from '../../targets'; -import { asPosix, getFiles, toCl } from '../../utils'; +import { asPosix, fromCl, getFiles, toCl } from '../../utils'; import { warningOut } from '../../cli'; import { name } from '../../../webpack.config'; import { FolderOptions, getFolderOptions } from './folderSettings'; import { readAllRules } from './customRules'; -import { CompileData, CommandParameters } from '../environment'; +import { CompileData, CommandParameters, getTrueBasename } from '../environment'; import { iProject } from '../iProject'; +import { ReadFileSystem } from '../../readFileSystem'; +import { ProjectActions } from '../actions'; export class MakeProject { private noChildren: boolean = false; private settings: iProject = new iProject(); + private projectActions: ProjectActions; + private folderSettings: {[folder: string]: FolderOptions} = {}; - constructor(private cwd: string, private targets: Targets) { - this.setupSettings(); + constructor(private cwd: string, private targets: Targets, private rfs: ReadFileSystem) { + this.projectActions = new ProjectActions(this.targets, this.rfs); } public setNoChildrenInBuild(noChildren: boolean) { this.noChildren = noChildren; } - private setupSettings() { + async setupSettings() { + await this.projectActions.loadAllActions(); + // First, let's setup the project settings try { - const content = readFileSync(path.join(this.cwd, `iproj.json`), { encoding: `utf-8` }); + const content = await this.rfs.readFile(path.join(this.cwd, `iproj.json`)); const asJson: iProject = JSON.parse(content); this.settings.applySettings(asJson); @@ -168,7 +174,7 @@ export class MakeProject { let lines = []; for (const entry of Object.entries(this.settings.compiles)) { - const [type, data] = entry; + let [type, data] = entry; // commandSource means 'is this object built from CL commands in a file' if (data.commandSource) { @@ -213,7 +219,22 @@ export class MakeProject { // This is used when your object really has source const possibleTarget: ILEObjectTarget = this.targets.getTarget(ileObject) || (ileObject as ILEObjectTarget); - const customAttributes = this.getObjectAttributes(data, possibleTarget); + let customAttributes = this.getObjectAttributes(data, possibleTarget); + + if (ileObject.relativePath) { + const possibleAction = this.projectActions.getActionForPath(ileObject.relativePath); + if (possibleAction) { + const clData = fromCl(possibleAction.command); + // If there is an action for this object, we want to apply the action's parameters + // to the custom attributes. + + data = { + ...data, + command: clData.command, + parameters: clData.parameters + } + } + } lines.push(...MakeProject.generateSpecificTarget(data, possibleTarget, customAttributes)); } @@ -250,12 +271,30 @@ export class MakeProject { const parentName = ileObject.relativePath ? path.dirname(ileObject.relativePath) : undefined; const qsysTempName: string | undefined = (parentName && parentName.length > 10 ? parentName.substring(0, 10) : parentName); + const simpleReplace = (str: string, search: string, replace: string) => { + return str.replace(new RegExp(search, `gi`), replace); + } + const resolve = (command: string) => { command = command.replace(new RegExp(`\\*CURLIB`, `g`), `$(BIN_LIB)`); command = command.replace(new RegExp(`\\$\\*`, `g`), ileObject.systemName); command = command.replace(new RegExp(`\\$<`, `g`), asPosix(ileObject.relativePath)); command = command.replace(new RegExp(`\\$\\(SRCPF\\)`, `g`), qsysTempName); + // Additionally, we have to support Actions variables + command = simpleReplace(command, `&BUILDLIB`, `$(BIN_LIB)`); + command = simpleReplace(command, `&CURLIB`, `$(BIN_LIB)`); + command = simpleReplace(command, `&LIBLS`, ``); + command = simpleReplace(command, `&BRANCHLIB`, `$(BIN_LIB)`); + + const pathDetail = path.parse(ileObject.relativePath || ``); + + command = simpleReplace(command, `&RELATIVEPATH`, asPosix(ileObject.relativePath)); + command = simpleReplace(command, `&BASENAME`, pathDetail.base); + command = simpleReplace(command, `{filename}`, pathDetail.base); + command = simpleReplace(command, `&NAME`, getTrueBasename(pathDetail.name)); + command = simpleReplace(command, `&EXTENSION`, pathDetail.ext.startsWith(`.`) ? pathDetail.ext.substring(1) : pathDetail.ext); + if (ileObject.deps && ileObject.deps.length > 0) { // This piece of code adds special variables that can be used for building dependencies const uniqueObjectTypes = ileObject.deps.map(d => d.type).filter((value, index, array) => array.indexOf(value) === index); diff --git a/cli/src/index.ts b/cli/src/index.ts index 8b7aec4..7a5776e 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -11,7 +11,6 @@ import { ImpactMarkdown } from "./builders/imd"; import { allExtensions, referencesFileName } from "./extensions"; import { getBranchLibraryName, getDefaultCompiles } from "./builders/environment"; import { renameFiles, replaceIncludes } from './utils'; -import { iProject } from './builders/iProject'; import { ReadFileSystem } from './readFileSystem'; const isCli = process.argv.length >= 2 && (process.argv[1].endsWith(`so`) || process.argv[1].endsWith(`index.js`)); @@ -40,11 +39,6 @@ async function main() { i++; break; - case `-i`: - case `--init`: - initProject(cwd); - process.exit(0); - case `-ar`: warningOut(`Auto rename enabled. No makefile will be generated.`) cliSettings.autoRename = true; @@ -126,11 +120,6 @@ async function main() { console.log(``); console.log(`Options specific to '-bf make':`); console.log(``); - console.log(`\t-i`); - console.log(`\t--init\t\tAdd default compile options to 'iproj.json' file`); - console.log(`\t\t\tShould be used for project initialisation or to customize compile commands.`); - console.log(`\t\t\tThis is specific to using '-bf' with the 'make' option.`); - console.log(``); console.log(`\t-nc`); console.log(`\t--no-children\tUsed with '-bf make' and won't include children of`); console.log(`\t\t\tobjects in the makefile. Useful in conjuction with '-f'.`); @@ -223,7 +212,9 @@ async function main() { break; case `make`: - const makeProj = new MakeProject(cwd, targets); + const makeProj = new MakeProject(cwd, targets, fs); + await makeProj.setupSettings(); + makeProj.setNoChildrenInBuild(cliSettings.makeFileNoChildren); let specificObjects: ILEObject[] | undefined = cliSettings.fileList ? cliSettings.lookupFiles.map(f => targets.getResolvedObject(path.join(cwd, f))).filter(o => o) : undefined; @@ -248,34 +239,6 @@ async function main() { } } -function initProject(cwd) { - console.log(`Initialising in ${cwd}`); - - const iprojPath = path.join(cwd, `iproj.json`); - - let base: Partial = {}; - const iprojExists = existsSync(iprojPath); - - if (iprojExists) { - try { - console.log(`iproj.json already exists. Will append new properties.`); - base = JSON.parse(readFileSync(iprojPath, { encoding: `utf-8` })); - } catch (e) { - error(`Failed to parse iproj.json. Aborting`); - process.exit(1); - } - } - - base = { - ...base, - compiles: getDefaultCompiles() - }; - - writeFileSync(iprojPath, JSON.stringify(base, null, 2)); - - console.log(`Written to ${iprojPath}`); -} - /** * @param query Can be object (ABCD.PGM) or relative path */ diff --git a/cli/src/readFileSystem.ts b/cli/src/readFileSystem.ts index 29de9d4..647bf8d 100644 --- a/cli/src/readFileSystem.ts +++ b/cli/src/readFileSystem.ts @@ -9,8 +9,8 @@ import { scanGlob } from './extensions'; export class ReadFileSystem { constructor() {} - async getFiles(cwd: string, globPath = scanGlob): Promise { - return getFiles(cwd, globPath); + async getFiles(cwd: string, globPath = scanGlob, additionalOpts: any = {}): Promise { + return getFiles(cwd, globPath, additionalOpts); } readFile(filePath: string): Promise { diff --git a/cli/src/targets.ts b/cli/src/targets.ts index 469675a..a384cb2 100644 --- a/cli/src/targets.ts +++ b/cli/src/targets.ts @@ -119,6 +119,10 @@ export class Targets { return this.cwd; } + get rfs() { + return this.fs; + } + public setAssumePrograms(assumePrograms: boolean) { this.assumePrograms = assumePrograms; } @@ -312,6 +316,7 @@ export class Targets { let globString = `**/${name}*`; + // TODO: replace with rfs.getFiles const results = glob.sync(globString, { cwd: this.cwd, absolute: true, @@ -683,6 +688,10 @@ export class Targets { files.forEach(def => { const possibleObject = def.file; if (possibleObject) { + if (possibleObject.library?.toUpperCase() === `*LIBL`) { + possibleObject.library = undefined; // This means lookup as normal + } + if (possibleObject.library) { this.logger.fileLog(ileObject.relativePath, { message: `Definition to ${possibleObject.library}/${possibleObject.name} ignored due to qualified path.`, diff --git a/cli/src/utils.ts b/cli/src/utils.ts index faa631b..45cf8d6 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -57,13 +57,14 @@ export function getSystemNameFromPath(inputName: string) { } /** - * @deprecated Use {@link ReadFileSystem.getFiles} instead + * @deprecated Use {@link ReadFileSystem} instead */ -export function getFiles(cwd: string, globPath: string): string[] { +export function getFiles(cwd: string, globPath: string, additionalOpts: any = {}): string[] { let paths: string[] = glob.sync(globPath, { cwd, absolute: true, nocase: true, + ...additionalOpts }); if (os.platform() === `win32`) { diff --git a/cli/test/cldclf.test.ts b/cli/test/cldclf.test.ts new file mode 100644 index 0000000..d6d7ff4 --- /dev/null +++ b/cli/test/cldclf.test.ts @@ -0,0 +1,45 @@ +import { beforeAll, describe, expect, test } from 'vitest'; + +import { Targets } from '../src/targets' +import { setupFixture } from './fixtures/projects'; +import { ReadFileSystem } from '../src/readFileSystem'; + +describe(`CL with DCLF`, () => { + const project = setupFixture(`cldclf`); + + const fs = new ReadFileSystem(); + const targets = new Targets(project.cwd, fs); + + beforeAll(async () => { + project.setup(); + await targets.loadProject(); + + expect(targets.getTargets().length).toBeGreaterThan(0); + targets.resolveBinder(); + }); + + test(`Objects are loaded`, () => { + expect(targets).toBeDefined(); + expect(targets.binderRequired()).toBeFalsy(); + + const targetObjects = targets.getTargets(); + + expect(targetObjects.length).toBe(2); + + expect(targetObjects.some(t => t.systemName === `APGM` && t.type === `PGM` && t.extension === `clle`)).toBeTruthy(); + expect(targetObjects.some(t => t.systemName === `DEPARTMENT` && t.type === `FILE` && t.extension === `table`)).toBeTruthy(); + }); + + test(`CL has valid dependency`, () => { + const apgm = targets.getTarget({systemName: `APGM`, type: `PGM`}); + expect(apgm).toBeDefined(); + + const logs = targets.logger.getLogsFor(apgm.relativePath); + console.log(logs); + expect(logs.length).toBe(0); + + expect(apgm.deps.length).toBe(1); + expect(apgm.deps[0].systemName).toBe(`DEPARTMENT`); + expect(apgm.deps[0].type).toBe(`FILE`); + }); +}); \ No newline at end of file diff --git a/cli/test/cs_srvpgm.test.ts b/cli/test/cs_srvpgm.test.ts index 8311599..c9c9409 100644 --- a/cli/test/cs_srvpgm.test.ts +++ b/cli/test/cs_srvpgm.test.ts @@ -6,6 +6,7 @@ import { setupFixture } from './fixtures/projects'; import { ReadFileSystem } from '../src/readFileSystem'; import { BobProject } from '../src/builders/bob'; import path from 'path'; +import { ProjectActions } from '../src/builders/actions'; describe(`pseudo tests`, () => { const project = setupFixture(`cs_srvpgm`); @@ -13,6 +14,7 @@ describe(`pseudo tests`, () => { const fs = new ReadFileSystem(); const targets = new Targets(project.cwd, fs); let make: MakeProject; + let actions: ProjectActions beforeAll(async () => { project.setup(); @@ -21,7 +23,11 @@ describe(`pseudo tests`, () => { expect(targets.getTargets().length).toBeGreaterThan(0); targets.resolveBinder(); - make = new MakeProject(project.cwd, targets); + make = new MakeProject(project.cwd, targets, fs); + await make.setupSettings(); + + actions = new ProjectActions(targets, fs); + await actions.loadAllActions(); }); test(`That test files are understood`, () => { @@ -70,8 +76,9 @@ describe(`pseudo tests`, () => { expect(files[path.join(`qrpglesrc`, `Rules.mk`)]).not.toContain(`EMPDET.SRVPGM`); // Ensure no service program is created }); - test('makefile', () => { - const makefile = new MakeProject(targets.getCwd(), targets); + test('makefile', async () => { + const makefile = new MakeProject(targets.getCwd(), targets, fs); + await makefile.setupSettings(); const contents = makefile.getMakefile().join(`\n`); @@ -80,5 +87,38 @@ describe(`pseudo tests`, () => { expect(contents).not.toContain(`EMPDET.SRVPGM`); // Ensure no service program is created expect(contents).toContain(`EMPDET.MODULE`); + + console.log(contents); + + // As picked up from the actions.json + expect(contents).toContain(`system "CRTBNDRPG NAME(mypgm) THEPARM('qrpglesrc/mypgm.pgm.rpgle')" > .logs/mypgm.splf`); + }); + + test('there are actions', async () => { + expect(actions.getActionPaths.length).toBe(2); + expect(actions.getActionPaths).toContain(`actions.json`); + expect(actions.getActionPaths).toContain(path.join(`qddssrc`, `actions.json`)); + }); + + test('correct actions get detected', async () => { + const ddsSrcA = path.join(`qddssrc`, `popemp.sql`); + const ddsSrcB = path.join(`qsqlsrc`, `popemp.sql`); + + // Finds the actions file `qddssrc` + const actionA = actions.getActionForPath(ddsSrcA); + + // Finds the project actions file + const actionB = actions.getActionForPath(ddsSrcB); + + expect(actionA).toBeDefined(); + expect(actionA?.command).toBe(`RUNSQLSTM SRCSTMF('&RELATIVEPATH')`); + + expect(actionB).toBeDefined(); + expect(actionB?.command).toBe(`RUNSQL SRCSTMF('&RELATIVEPATH')`); + + const rpgleSrc = path.join(`qrpglesrc`, `mypgm.pgm.rpgle`); + const actionC = actions.getActionForPath(rpgleSrc); + expect(actionC).toBeDefined(); + expect(actionC?.command).toBe(`CRTBNDRPG NAME(&NAME) THEPARM('&RELATIVEPATH')`); }); }); diff --git a/cli/test/cs_with_bnddir.test.ts b/cli/test/cs_with_bnddir.test.ts index fa88496..590b6a5 100644 --- a/cli/test/cs_with_bnddir.test.ts +++ b/cli/test/cs_with_bnddir.test.ts @@ -21,7 +21,8 @@ describe(`pseudo tests`, () => { expect(targets.getTargets().length).toBeGreaterThan(0); targets.resolveBinder(); - make = new MakeProject(project.cwd, targets); + make = new MakeProject(project.cwd, targets, fs); + await make.setupSettings(); }); test(`That test files are understood`, () => { @@ -54,8 +55,9 @@ describe(`pseudo tests`, () => { expect(employees.deps.find(f => f.systemName === `EMPLOYEE` && f.type === `FILE`)).toBeDefined(); }); - test('makefile', () => { - const makefile = new MakeProject(targets.getCwd(), targets); + test('makefile', async () => { + const makefile = new MakeProject(targets.getCwd(), targets, fs); + await make.setupSettings(); const contents = makefile.getMakefile().join(`\n`); diff --git a/cli/test/ddsDeps.test.ts b/cli/test/ddsDeps.test.ts index 44013c0..777f307 100644 --- a/cli/test/ddsDeps.test.ts +++ b/cli/test/ddsDeps.test.ts @@ -80,8 +80,9 @@ describe(`dds_refs tests`, () => { expect(logs).toBeUndefined(); }); - test(`make doesn't include refs that do not exist`, () => { - const makeProject = new MakeProject(project.cwd, targets); + test(`make doesn't include refs that do not exist`, async () => { + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); const targetContent = makeProject.generateTargets(); diff --git a/cli/test/ddsDepsWithRefFile.test.ts b/cli/test/ddsDepsWithRefFile.test.ts index 4157a38..33c41e3 100644 --- a/cli/test/ddsDepsWithRefFile.test.ts +++ b/cli/test/ddsDepsWithRefFile.test.ts @@ -84,8 +84,9 @@ describe(`dds_refs tests with reference file`, () => { }); - test(`make doesn't include refs that do not exist or are referenced objects`, () => { - const makeProject = new MakeProject(project.cwd, targets); + test(`make doesn't include refs that do not exist or are referenced objects`, async () => { + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); const targetContent = makeProject.generateTargets(); diff --git a/cli/test/fixtures/cldclf/apgm.pgm.clle b/cli/test/fixtures/cldclf/apgm.pgm.clle new file mode 100644 index 0000000..36fd445 --- /dev/null +++ b/cli/test/fixtures/cldclf/apgm.pgm.clle @@ -0,0 +1,3 @@ +PGM + DCLF FILE(*LIBL/DEPARTMENT) OPNID(SAVRSTLIB) +ENDPGM \ No newline at end of file diff --git a/cli/test/fixtures/cldclf/department.table b/cli/test/fixtures/cldclf/department.table new file mode 100644 index 0000000..d6c3b0f --- /dev/null +++ b/cli/test/fixtures/cldclf/department.table @@ -0,0 +1,29 @@ +--https://www.ibm.com/docs/en/i/7.3?topic=tables-department-table-department + +CREATE OR REPLACE TABLE DEPARTMENT + (DEPTNO CHAR(3) NOT NULL, + DEPTNAME VARCHAR(36) NOT NULL, + MGRNO CHAR(6) NOT NULL, + ADMRDEPT CHAR(3) NOT NULL, + LOCATION CHAR(16) NOT NULL, + PRIMARY KEY (DEPTNO)); + +ALTER TABLE DEPARTMENT + ADD FOREIGN KEY ROD (ADMRDEPT) + REFERENCES DEPARTMENT + ON DELETE CASCADE; + +-- Remove circular reference +--ALTER TABLE DEPARTMENT +-- ADD FOREIGN KEY RDE (MGRNO) +-- REFERENCES EMPLOYEE +-- ON DELETE SET NULL; + +-- CREATE UNIQUE INDEX XDEPT1 +-- ON DEPARTMENT (DEPTNO); + +-- CREATE INDEX XDEPT2 +-- ON DEPARTMENT (MGRNO); + +-- CREATE INDEX XDEPT3 +-- ON DEPARTMENT (ADMRDEPT); \ No newline at end of file diff --git a/cli/test/fixtures/cs_srvpgm/.vscode/actions.json b/cli/test/fixtures/cs_srvpgm/.vscode/actions.json new file mode 100644 index 0000000..3aa46a1 --- /dev/null +++ b/cli/test/fixtures/cs_srvpgm/.vscode/actions.json @@ -0,0 +1,18 @@ +[ + { + "name": "Create an RPG program", + "environment": "ile", + "extensions": [ + "rpgle" + ], + "command": "CRTBNDRPG NAME(&NAME) THEPARM('&RELATIVEPATH')" + }, + { + "name": "Global run SQL", + "environment": "ile", + "extensions": [ + "sql" + ], + "command": "RUNSQL SRCSTMF('&RELATIVEPATH')" + } +] \ No newline at end of file diff --git a/cli/test/fixtures/cs_srvpgm/qddssrc/actions.json b/cli/test/fixtures/cs_srvpgm/qddssrc/actions.json new file mode 100644 index 0000000..4f001d2 --- /dev/null +++ b/cli/test/fixtures/cs_srvpgm/qddssrc/actions.json @@ -0,0 +1,10 @@ +[ + { + "name": "Run SQL stuff", + "environment": "ile", + "extensions": [ + "sql", "table" + ], + "command": "RUNSQLSTM SRCSTMF('&RELATIVEPATH')" + } +] \ No newline at end of file diff --git a/cli/test/make.test.ts b/cli/test/make.test.ts index 0274755..c6869f9 100644 --- a/cli/test/make.test.ts +++ b/cli/test/make.test.ts @@ -1,10 +1,12 @@ import { assert, expect, test } from 'vitest' import { baseTargets, cwd, multiModuleObjects } from './fixtures/targets'; import { MakeProject } from '../src/builders/make'; +import { ReadFileSystem } from '../src/readFileSystem'; test('generateTargets (pre-resolve)', async () => { const targets = await baseTargets(true); - const project = new MakeProject(cwd, targets); + const project = new MakeProject(cwd, targets, new ReadFileSystem()); + await project.setupSettings(); const targetContent = project.generateTargets(); @@ -32,7 +34,7 @@ test('generateTargets (post-resolve)', async () => { targets.resolveBinder(); - const project = new MakeProject(cwd, targets); + const project = new MakeProject(cwd, targets, new ReadFileSystem()); const targetContent = project.generateTargets(); @@ -62,7 +64,8 @@ test('generateTargets (post-resolve)', async () => { test('generateHeader (binder changes)', async () => { const targets = await baseTargets(true); - const project = new MakeProject(cwd, targets); + const project = new MakeProject(cwd, targets, new ReadFileSystem()); + await project.setupSettings(); const headerContentA = project.generateHeader(); let bndDirIndex = headerContentA.findIndex(h => h.startsWith(`BNDDIR=`)); @@ -80,7 +83,8 @@ test('generateHeader (binder changes)', async () => { test('applySettings (binder)', async () => { const targets = await baseTargets(true); - const project = new MakeProject(cwd, targets); + const project = new MakeProject(cwd, targets, new ReadFileSystem()); + await project.setupSettings(); project.getSettings().applySettings({ binders: [`TESTING`] @@ -102,7 +106,9 @@ test('applySettings (binder)', async () => { test(`Multi-module program and service program`, async () => { const targets = await multiModuleObjects(); - const project = new MakeProject(cwd, targets); + const project = new MakeProject(cwd, targets, new ReadFileSystem()); + await project.setupSettings(); + const settings = project.getSettings(); settings.applySettings({ diff --git a/cli/test/multiModule.test.ts b/cli/test/multiModule.test.ts index fd85c6c..a661847 100644 --- a/cli/test/multiModule.test.ts +++ b/cli/test/multiModule.test.ts @@ -48,8 +48,9 @@ describe(`multi_module tests`, () => { expect(objectsMod.deps.length).toBe(0); }); - test(`Generate makefile`, () => { - const makeProj = new MakeProject(project.cwd, targets); + test(`Generate makefile`, async () => { + const makeProj = new MakeProject(project.cwd, targets, fs); + await makeProj.setupSettings(); writeFileSync(path.join(project.cwd, `makefile`), makeProj.getMakefile().join(`\n`)); }); diff --git a/cli/test/multiModule2.test.ts b/cli/test/multiModule2.test.ts index a111ee6..0a41fc9 100644 --- a/cli/test/multiModule2.test.ts +++ b/cli/test/multiModule2.test.ts @@ -53,7 +53,8 @@ describe(`multi_module_two tests`, () => { }); test(`Check makefile result`, async () => { - const makeProject = new MakeProject(project.cwd, targets); + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); const targetContent = makeProject.getMakefile(); diff --git a/cli/test/project.test.ts b/cli/test/project.test.ts index 7186602..e43b837 100644 --- a/cli/test/project.test.ts +++ b/cli/test/project.test.ts @@ -272,8 +272,9 @@ describe(`company_system tests`, () => { ].join()); }); - test(`Checking makefile rule generation`, () => { - const makeProject = new MakeProject(project.cwd, targets); + test(`Checking makefile rule generation`, async () => { + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); const headerContent = makeProject.generateGenericRules(); @@ -283,8 +284,9 @@ describe(`company_system tests`, () => { expect(headerContent.find(l => l === `$(PREPATH)/DEPARTMENT.FILE: qddssrc/department.table`)).toBeDefined(); }); - test(`Makefile targets for all`, () => { - const makeProject = new MakeProject(project.cwd, targets); + test(`Makefile targets for all`, async () => { + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); const header = makeProject.generateHeader(); expect(header).toContain(`APP_BNDDIR=$(APP_BNDDIR)`); @@ -303,8 +305,9 @@ describe(`company_system tests`, () => { expect(allTarget).toContain(`$(PREPATH)/EMPLOYEES.PGM`); }); - test(`Makefile targets for partial build (DEPTS display file)`, () => { - const makeProject = new MakeProject(project.cwd, targets); + test(`Makefile targets for partial build (DEPTS display file)`, async () => { + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); const deptsFile = targets.getTarget({systemName: `DEPTS`, type: `FILE`}); @@ -317,8 +320,9 @@ describe(`company_system tests`, () => { expect(allTarget).toBe(`all: .logs .evfevent library $(PREPATH)/DEPTS.FILE $(PREPATH)/DEPTS.PGM`); }); - test(`Makefile targets for partial build (EMPLOYEE table)`, () => { - const makeProject = new MakeProject(project.cwd, targets); + test(`Makefile targets for partial build (EMPLOYEE table)`, async () => { + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); const deptsFile = targets.getTarget({systemName: `EMPLOYEE`, type: `FILE`}); @@ -345,8 +349,10 @@ describe(`company_system tests`, () => { expect(deptsTargetDeps).toContain(`$(PREPATH)/DEPARTMENT.FILE`); }); - test(`Makefile targets for partial build (EMPLOYEE table) without children`, () => { - const makeProject = new MakeProject(project.cwd, targets); + test(`Makefile targets for partial build (EMPLOYEE table) without children`, async () => { + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); + makeProject.setNoChildrenInBuild(true); const deptsFile = targets.getTarget({systemName: `EMPLOYEE`, type: `FILE`}); @@ -447,8 +453,9 @@ describe(`company_system tests`, () => { expect(functionMake[3]).toBe(`\tsystem "RUNSQLSTM SRCSTMF('qsqlsrc/getTotalSalary.sqludf') COMMIT(*NONE)" > .logs/gettotsal.splf`); }) - test(`Generate makefile`, () => { - const makeProj = new MakeProject(project.cwd, targets); + test(`Generate makefile`, async () => { + const makeProj = new MakeProject(project.cwd, targets, fs); + await makeProj.setupSettings(); writeFileSync(path.join(project.cwd, `makefile`), makeProj.getMakefile().join(`\n`)); }); diff --git a/cli/test/project2.test.ts b/cli/test/project2.test.ts index 081aa63..16d7443 100644 --- a/cli/test/project2.test.ts +++ b/cli/test/project2.test.ts @@ -268,8 +268,9 @@ describe(`company_system tests`, () => { ].join()); }); - test(`Checking makefile rule generation`, () => { - const makeProject = new MakeProject(project.cwd, targets); + test(`Checking makefile rule generation`, async () => { + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); const headerContent = makeProject.generateGenericRules(); @@ -279,8 +280,9 @@ describe(`company_system tests`, () => { expect(headerContent.find(l => l === `$(PREPATH)/DEPARTMENT.FILE: qddssrc/department.table`)).toBeDefined(); }); - test(`Makefile targets for all`, () => { - const makeProject = new MakeProject(project.cwd, targets); + test(`Makefile targets for all`, async () => { + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); // Generate targets on it's own will have BNDDIR, PGM, etc const headerContent = makeProject.generateTargets(); @@ -296,8 +298,9 @@ describe(`company_system tests`, () => { expect(allTarget).toContain(`$(PREPATH)/EMPLOYEES.PGM`); }); - test(`Makefile targets for partial build (DEPTS display file)`, () => { - const makeProject = new MakeProject(project.cwd, targets); + test(`Makefile targets for partial build (DEPTS display file)`, async () => { + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); const deptsFile = targets.getTarget({systemName: `DEPTS`, type: `FILE`}); @@ -310,8 +313,9 @@ describe(`company_system tests`, () => { expect(allTarget).toBe(`all: .logs .evfevent library $(PREPATH)/DEPTS.FILE $(PREPATH)/DEPTS.PGM`); }); - test(`Makefile targets for partial build (EMPLOYEE table)`, () => { - const makeProject = new MakeProject(project.cwd, targets); + test(`Makefile targets for partial build (EMPLOYEE table)`, async () => { + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); const deptsFile = targets.getTarget({systemName: `EMPLOYEE`, type: `FILE`}); @@ -338,8 +342,10 @@ describe(`company_system tests`, () => { expect(deptsTargetDeps).toContain(`$(PREPATH)/DEPARTMENT.FILE`); }); - test(`Makefile targets for partial build (EMPLOYEE table) without children`, () => { - const makeProject = new MakeProject(project.cwd, targets); + test(`Makefile targets for partial build (EMPLOYEE table) without children`, async () => { + const makeProject = new MakeProject(project.cwd, targets, fs); + await makeProject.setupSettings(); + makeProject.setNoChildrenInBuild(true); const deptsFile = targets.getTarget({systemName: `EMPLOYEE`, type: `FILE`}); @@ -440,8 +446,9 @@ describe(`company_system tests`, () => { expect(functionMake[3]).toBe(`\tsystem "RUNSQLSTM SRCSTMF('qsqlsrc/getTotalSalary.sqludf') COMMIT(*NONE)" > .logs/gettotsal.splf`); }) - test(`Generate makefile`, () => { - const makeProj = new MakeProject(project.cwd, targets); + test(`Generate makefile`, async () => { + const makeProj = new MakeProject(project.cwd, targets, fs); + await makeProj.setupSettings(); writeFileSync(path.join(project.cwd, `makefile`), makeProj.getMakefile().join(`\n`)); }); diff --git a/cli/test/pseudo.test.ts b/cli/test/pseudo.test.ts index 2435cb9..46d10d2 100644 --- a/cli/test/pseudo.test.ts +++ b/cli/test/pseudo.test.ts @@ -19,7 +19,8 @@ describe(`pseudo tests`, () => { expect(targets.getTargets().length).toBeGreaterThan(0); targets.resolveBinder(); - make = new MakeProject(project.cwd, targets); + make = new MakeProject(project.cwd, targets, fs); + await make.setupSettings(); }); test(`Test objects exists`, () => { diff --git a/docs/pages/cli/make.md b/docs/pages/cli/make.md index 07ed6b6..b78373d 100644 --- a/docs/pages/cli/make.md +++ b/docs/pages/cli/make.md @@ -26,7 +26,7 @@ So that means that 4 objects are going to be rebuilt. Usually, parents always ne ### When is a incremental build right? -It is always recommended to do a incremental build when working in a new branch. Ensure that you have a library of objects from a previous full build on the library list! +It is always recommended to do a incremental build when working in a new branch. Ensure that you have a library of objects from a previous full build on the library list. ## `iproj.json` properties @@ -34,15 +34,49 @@ Source Orbit makes use of a few properties that you can put inside your `iproj.j | Property | Type | | -------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `binders` | Array of strings. Use this if you want to use export functions/procedures from other service programs outside of your repository | | `includePaths` | Array of strings. Use this if you want to include headers that can be found in the IFS at compile time. **You do not need to list directories that are part of the repository.** | ## Binding handled automatically If you are making use of service programs, Source Orbit will automatically maintain this for you. Your service programs will be added to the binding directory automatically and programs automatically include them. **No need to use the `BNDDIR` header** in your source code. Be careful, because changing the custom compile commands from the default may break this functionality. **You should still create binder source for all your service programs** in order for Source Orbit to create them automatically. +If your project contains a `.bnddir` file, then the basename of that file will be used as the binding directory name. If you do not have a `.bnddir` file, then the binding directory will be named `APP` automatically. + ## Changing compile options +When you use `so -bf make`, Source Orbit will generate a makefile that contains the compile commands for each object. The compile commands can come from two places + +### Setting up generic compiles (`actions.json`) + +The `actions.json` standard determines how files that match specific extensions are compiled. The file is typically created by a user and is located in `.vscode/actions.json`. The `actions.json` standard comes from Code for IBM i and is now used by Source Orbit. + +Check out how to use the `actions.json` file in the [Code for IBM i documentation](https://codefori.github.io/docs/developing/local/actions/). + +#### Multiple levels of actions + +The `.vscode/actions.json` is the default location for the `actions.json` file. However, you can also create a `actions.json` file in a specific directory, which will be merged with the default one. This allows you to have directory specific actions, while still using the defaults. + +``` +.vscode: + actions.json ## Actions defined here apply to the entire project + +qddssrc: + actions.json ## Actions defined here only apply to qddssrc + department.table + employee.table + emps.dspf + nemp.dspf + +qrpglesrc: + depts.pgm.sqlrpgle + empdet.sqlrpgle + employees.pgm.sqlrpgle + mypgm.pgm.rpgle + newemp.pgm.sqlrpgle +``` + +### Changing project wide compiles (`iproj.json`) + The `iproj.json` can contain a `compiles` property to customize how each extension type gets compiled. To generate the defaults, you can use `so --init` to create or update the `iproj.json` file. When you define an extension in the `compiles` property, the properties of it will override the `so` defaults. Here is the schema for the compiles option: @@ -68,7 +102,7 @@ interface CompileData { }; ``` -### Example: build object from commands +#### Example: build object from commands Objects like data areas do not have source. In the `compiles` property, there is a flag that can be used to tell Source Orbit to get the commands from the source. diff --git a/vs/client/package-lock.json b/vs/client/package-lock.json index f0a6cf3..9d9c0bd 100644 --- a/vs/client/package-lock.json +++ b/vs/client/package-lock.json @@ -24,7 +24,7 @@ }, "../../cli": { "name": "@ibm/sourceorbit", - "version": "1.0.4", + "version": "1.1.0", "dev": true, "license": "Apache 2", "dependencies": { diff --git a/vs/package-lock.json b/vs/package-lock.json index 9909062..b077f35 100644 --- a/vs/package-lock.json +++ b/vs/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-sourceorbit", - "version": "1.0.2", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-sourceorbit", - "version": "1.0.2", + "version": "1.1.0", "hasInstallScript": true, "license": "MIT", "devDependencies": { diff --git a/vs/server/package-lock.json b/vs/server/package-lock.json index 62365b7..343abc2 100644 --- a/vs/server/package-lock.json +++ b/vs/server/package-lock.json @@ -24,7 +24,7 @@ }, "../../cli": { "name": "@ibm/sourceorbit", - "version": "1.0.4", + "version": "1.1.0", "license": "Apache 2", "dependencies": { "crc-32": "https://cdn.sheetjs.com/crc-32-latest/crc-32-latest.tgz" diff --git a/vs/server/src/setup.ts b/vs/server/src/setup.ts index a1cfc03..9a8c4c1 100644 --- a/vs/server/src/setup.ts +++ b/vs/server/src/setup.ts @@ -51,7 +51,9 @@ export async function generateBuildFile(workspaceUri: string, type: string) { break; case `make`: - const makeProj = new MakeProject(cwd, targets); + const makeProj = new MakeProject(cwd, targets, targets.rfs); + await makeProj.setupSettings(); + fs.writeFileSync(path.join(cwd, `makefile`), makeProj.getMakefile().join(`\n`)); break;